/**
 * Plupload - multi-runtime File Uploader
 * v3.1.2
 *
 * Copyright 2018, Ephox
 * Released under AGPLv3 License.
 *
 * License: http://www.plupload.com/license
 * Contributing: http://www.plupload.com/contributing
 *
 * Date: 2018-02-20
 */
;(function (global, factory) {
    var extract = function () {
        var ctx = {};
        factory.apply(ctx, arguments);
        return ctx.plupload;
    };

    if (typeof define === "function" && define.amd) {
        define("plupload", ['./moxie'], extract);
    } else if (typeof module === "object" && module.exports) {
        module.exports = extract(require('./moxie'));
    } else {
        global.plupload = extract(global.moxie);
    }
}(this || window, function (moxie) {
    /**
     * Compiled inline version. (Library mode)
     */

    /*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */
    /*globals $code */

    (function (exports, undefined) {
        "use strict";

        var modules = {};

        function require(ids, callback) {
            var module, defs = [];

            for (var i = 0; i < ids.length; ++i) {
                module = modules[ids[i]] || resolve(ids[i]);
                if (!module) {
                    throw 'module definition dependecy not found: ' + ids[i];
                }

                defs.push(module);
            }

            callback.apply(null, defs);
        }

        function define(id, dependencies, definition) {
            if (typeof id !== 'string') {
                throw 'invalid module definition, module id must be defined and be a string';
            }

            if (dependencies === undefined) {
                throw 'invalid module definition, dependencies must be specified';
            }

            if (definition === undefined) {
                throw 'invalid module definition, definition function must be specified';
            }

            require(dependencies, function () {
                modules[id] = definition.apply(null, arguments);
            });
        }

        function defined(id) {
            return !!modules[id];
        }

        function resolve(id) {
            var target = exports;
            var fragments = id.split(/[.\/]/);

            for (var fi = 0; fi < fragments.length; ++fi) {
                if (!target[fragments[fi]]) {
                    return;
                }

                target = target[fragments[fi]];
            }

            return target;
        }

        function expose(ids) {
            for (var i = 0; i < ids.length; i++) {
                var target = exports;
                var id = ids[i];
                var fragments = id.split(/[.\/]/);

                for (var fi = 0; fi < fragments.length - 1; ++fi) {
                    if (target[fragments[fi]] === undefined) {
                        target[fragments[fi]] = {};
                    }

                    target = target[fragments[fi]];
                }

                target[fragments[fragments.length - 1]] = modules[id];
            }
        }

// Included from: src/plupload.js

        /**
         * plupload.js
         *
         * Copyright 2017, Ephox
         * Released under AGPLv3 License.
         *
         * License: http://www.plupload.com/license
         * Contributing: http://www.plupload.com/contributing
         */

        /**
         Namespace for all Plupload related classes, methods and properties.

         @class plupload
         @public
         @static
         */
        define('plupload', [], function () {

            var o = moxie;
            var u = o.core.utils;

            // redifine event dispatcher for Flash/Silverlight runtimes
            u.Env.global_event_dispatcher = 'plupload.EventTarget.instance.dispatchEvent';


            return {
                /**
                 * Plupload version will be replaced on build.
                 *
                 * @property VERSION
                 * @static
                 * @final
                 */
                VERSION: '3.1.2',

                /**
                 * The state of the queue before it has started and after it has finished
                 *
                 * @property STOPPED
                 * @static
                 * @final
                 */
                STOPPED: 1,

                /**
                 * Upload process is running
                 *
                 * @property STARTED
                 * @static
                 * @final
                 */
                STARTED: 2,

                /**
                 File is queued for upload

                 @property QUEUED
                 @static
                 @final
                 */
                QUEUED: 1,

                /**
                 File is being uploaded

                 @property UPLOADING
                 @static
                 @final
                 */
                UPLOADING: 2,

                /**
                 File has failed to be uploaded

                 @property FAILED
                 @static
                 @final
                 */
                FAILED: 4,

                /**
                 File has been uploaded successfully

                 @property DONE
                 @static
                 @final
                 */
                DONE: 5,

                // Error constants used by the Error event

                /**
                 * Generic error for example if an exception is thrown inside Silverlight.
                 *
                 * @property GENERIC_ERROR
                 * @static
                 * @final
                 */
                GENERIC_ERROR: -100,

                /**
                 * HTTP transport error. For example if the server produces a HTTP status other than 200.
                 *
                 * @property HTTP_ERROR
                 * @static
                 * @final
                 */
                HTTP_ERROR: -200,

                /**
                 * Generic I/O error. For example if it wasn't possible to open the file stream on local machine.
                 *
                 * @property IO_ERROR
                 * @static
                 * @final
                 */
                IO_ERROR: -300,

                /**
                 * @property SECURITY_ERROR
                 * @static
                 * @final
                 */
                SECURITY_ERROR: -400,

                /**
                 * Initialization error. Will be triggered if no runtime was initialized.
                 *
                 * @property INIT_ERROR
                 * @static
                 * @final
                 */
                INIT_ERROR: -500,

                /**
                 * File size error. If the user selects a file that is too large it will be blocked and an error of this type will be triggered.
                 *
                 * @property FILE_SIZE_ERROR
                 * @static
                 * @final
                 */
                FILE_SIZE_ERROR: -600,

                /**
                 * File extension error. If the user selects a file that isn't valid according to the filters setting.
                 *
                 * @property FILE_EXTENSION_ERROR
                 * @static
                 * @final
                 */
                FILE_EXTENSION_ERROR: -601,

                /**
                 * Duplicate file error. If prevent_duplicates is set to true and user selects the same file again.
                 *
                 * @property FILE_DUPLICATE_ERROR
                 * @static
                 * @final
                 */
                FILE_DUPLICATE_ERROR: -602,

                /**
                 * Runtime will try to detect if image is proper one. Otherwise will throw this error.
                 *
                 * @property IMAGE_FORMAT_ERROR
                 * @static
                 * @final
                 */
                IMAGE_FORMAT_ERROR: -700,

                /**
                 * While working on files runtime may run out of memory and will throw this error.
                 *
                 * @since 2.1.2
                 * @property MEMORY_ERROR
                 * @static
                 * @final
                 */
                MEMORY_ERROR: -701,

                /**
                 * Each runtime has an upper limit on a dimension of the image it can handle. If bigger, will throw this error.
                 *
                 * @property IMAGE_DIMENSIONS_ERROR
                 * @static
                 * @final
                 */
                IMAGE_DIMENSIONS_ERROR: -702,


                /**
                 Invalid option error. Will be thrown if user tries to alter the option that cannot be changed without
                 uploader reinitialisation.

                 @property OPTION_ERROR
                 @static
                 @final
                 */
                OPTION_ERROR: -800,

                /**
                 * Expose whole moxie (#1469).
                 *
                 * @property moxie
                 * @type Object
                 * @final
                 */
                moxie: o,

                /**
                 * In some cases sniffing is the only way around :(
                 */
                ua: u.Env,

                /**
                 * Gets the true type of the built-in object (better version of typeof).
                 * @credits Angus Croll (http://javascriptweblog.wordpress.com/)
                 *
                 * @method typeOf
                 * @static
                 * @param {Object} o Object to check.
                 * @return {String} Object [[Class]]
                 */
                typeOf: u.Basic.typeOf,

                clone: u.Basic.clone,

                inherit: u.Basic.inherit,


                /**
                 * Extends the specified object with another object.
                 *
                 * @method extend
                 * @static
                 * @param {Object} target Object to extend.
                 * @param {Object..} obj Multiple objects to extend with.
                 * @return {Object} Same as target, the extended object.
                 */
                extend: u.Basic.extend,


                extendImmutable: u.Basic.extendImmutable,

                /**
                 Extends the specified object with another object(s), but only if the property exists in the target.

                 @method extendIf
                 @static
                 @param {Object} target Object to extend.
                 @param {Object} [obj]* Multiple objects to extend with.
                 @return {Object} Same as target, the extended object.
                 */
                extendIf: u.Basic.extendIf,

                /**
                 Recieve an array of functions (usually async) to call in sequence, each  function
                 receives a callback as first argument that it should call, when it completes. Finally,
                 after everything is complete, main callback is called. Passing truthy value to the
                 callback as a first argument will interrupt the sequence and invoke main callback
                 immediately.

                 @method inSeries
                 @static
                 @param {Array} queue Array of functions to call in sequence
                 @param {Function} cb Main callback that is called in the end, or in case of error
                 */
                inSeries: u.Basic.inSeries,

                /**
                 Recieve an array of functions (usually async) to call in parallel, each  function
                 receives a callback as first argument that it should call, when it completes. After
                 everything is complete, main callback is called. Passing truthy value to the
                 callback as a first argument will interrupt the process and invoke main callback
                 immediately.

                 @method inParallel
                 @static
                 @param {Array} queue Array of functions to call in sequence
                 @param {Function} cb Main callback that is called in the end, or in case of erro
                 */
                inParallel: u.Basic.inParallel,

                /**
                 * Generates an unique ID. This is 99.99% unique since it takes the current time and 5 random numbers.
                 * The only way a user would be able to get the same ID is if the two persons at the same exact millisecond manages
                 * to get 5 the same random numbers between 0-65535 it also uses a counter so each call will be guaranteed to be page unique.
                 * It's more probable for the earth to be hit with an asteriod. You can also if you want to be 100% sure set the plupload.guidPrefix property
                 * to an user unique key.
                 *
                 * @method guid
                 * @static
                 * @return {String} Virtually unique id.
                 */
                guid: u.Basic.guid,

                /**
                 * Get array of DOM Elements by their ids.
                 *
                 * @method get
                 * @param {String} id Identifier of the DOM Element
                 * @return {Array}
                 */
                getAll: function get(ids) {
                    var els = [],
                        el;

                    if (u.Basic.typeOf(ids) !== 'array') {
                        ids = [ids];
                    }

                    var i = ids.length;
                    while (i--) {
                        el = u.Dom.get(ids[i]);
                        if (el) {
                            els.push(el);
                        }
                    }

                    return els.length ? els : null;
                },

                /**
                 Get DOM element by id

                 @method get
                 @param {String} id Identifier of the DOM Element
                 @return {Node}
                 */
                get: u.Dom.get,

                /**
                 * Executes the callback function for each item in array/object. If you return false in the
                 * callback it will break the loop.
                 *
                 * @method each
                 * @static
                 * @param {Object} obj Object to iterate.
                 * @param {function} callback Callback function to execute for each item.
                 */
                each: u.Basic.each,

                /**
                 * Returns the absolute x, y position of an Element. The position will be returned in a object with x, y fields.
                 *
                 * @method getPos
                 * @static
                 * @param {Element} node HTML element or element id to get x, y position from.
                 * @param {Element} root Optional root element to stop calculations at.
                 * @return {object} Absolute position of the specified element object with x, y fields.
                 */
                getPos: u.Dom.getPos,

                /**
                 * Returns the size of the specified node in pixels.
                 *
                 * @method getSize
                 * @static
                 * @param {Node} node Node to get the size of.
                 * @return {Object} Object with a w and h property.
                 */
                getSize: u.Dom.getSize,

                /**
                 * Encodes the specified string.
                 *
                 * @method xmlEncode
                 * @static
                 * @param {String} s String to encode.
                 * @return {String} Encoded string.
                 */
                xmlEncode: function (str) {
                    var xmlEncodeChars = {
                            '<': 'lt',
                            '>': 'gt',
                            '&': 'amp',
                            '"': 'quot',
                            '\'': '#39'
                        },
                        xmlEncodeRegExp = /[<>&\"\']/g;

                    return str ? ('' + str).replace(xmlEncodeRegExp, function (chr) {
                        return xmlEncodeChars[chr] ? '&' + xmlEncodeChars[chr] + ';' : chr;
                    }) : str;
                },

                /**
                 * Forces anything into an array.
                 *
                 * @method toArray
                 * @static
                 * @param {Object} obj Object with length field.
                 * @return {Array} Array object containing all items.
                 */
                toArray: u.Basic.toArray,

                /**
                 * Find an element in array and return its index if present, otherwise return -1.
                 *
                 * @method inArray
                 * @static
                 * @param {mixed} needle Element to find
                 * @param {Array} array
                 * @return {Int} Index of the element, or -1 if not found
                 */
                inArray: u.Basic.inArray,

                /**
                 * Extends the language pack object with new items.
                 *
                 * @method addI18n
                 * @static
                 * @param {Object} pack Language pack items to add.
                 * @return {Object} Extended language pack object.
                 */
                addI18n: o.core.I18n.addI18n,

                /**
                 * Translates the specified string by checking for the english string in the language pack lookup.
                 *
                 * @method translate
                 * @static
                 * @param {String} str String to look for.
                 * @return {String} Translated string or the input string if it wasn't found.
                 */
                translate: o.core.I18n.translate,

                /**
                 * Pseudo sprintf implementation - simple way to replace tokens with specified values.
                 *
                 * @param {String} str String with tokens
                 * @return {String} String with replaced tokens
                 */
                sprintf: u.Basic.sprintf,

                /**
                 * Checks if object is empty.
                 *
                 * @method isEmptyObj
                 * @static
                 * @param {Object} obj Object to check.
                 * @return {Boolean}
                 */
                isEmptyObj: u.Basic.isEmptyObj,

                /**
                 * Checks if specified DOM element has specified class.
                 *
                 * @method hasClass
                 * @static
                 * @param {Object} obj DOM element like object to add handler to.
                 * @param {String} name Class name
                 */
                hasClass: u.Dom.hasClass,

                /**
                 * Adds specified className to specified DOM element.
                 *
                 * @method addClass
                 * @static
                 * @param {Object} obj DOM element like object to add handler to.
                 * @param {String} name Class name
                 */
                addClass: u.Dom.addClass,

                /**
                 * Removes specified className from specified DOM element.
                 *
                 * @method removeClass
                 * @static
                 * @param {Object} obj DOM element like object to add handler to.
                 * @param {String} name Class name
                 */
                removeClass: u.Dom.removeClass,

                /**
                 * Returns a given computed style of a DOM element.
                 *
                 * @method getStyle
                 * @static
                 * @param {Object} obj DOM element like object.
                 * @param {String} name Style you want to get from the DOM element
                 */
                getStyle: u.Dom.getStyle,

                /**
                 * Adds an event handler to the specified object and store reference to the handler
                 * in objects internal Plupload registry (@see removeEvent).
                 *
                 * @method addEvent
                 * @static
                 * @param {Object} obj DOM element like object to add handler to.
                 * @param {String} name Name to add event listener to.
                 * @param {Function} callback Function to call when event occurs.
                 * @param {String} (optional) key that might be used to add specifity to the event record.
                 */
                addEvent: u.Events.addEvent,

                /**
                 * Remove event handler from the specified object. If third argument (callback)
                 * is not specified remove all events with the specified name.
                 *
                 * @method removeEvent
                 * @static
                 * @param {Object} obj DOM element to remove event listener(s) from.
                 * @param {String} name Name of event listener to remove.
                 * @param {Function|String} (optional) might be a callback or unique key to match.
                 */
                removeEvent: u.Events.removeEvent,

                /**
                 * Remove all kind of events from the specified object
                 *
                 * @method removeAllEvents
                 * @static
                 * @param {Object} obj DOM element to remove event listeners from.
                 * @param {String} (optional) unique key to match, when removing events.
                 */
                removeAllEvents: u.Events.removeAllEvents,

                /**
                 * Cleans the specified name from national characters (diacritics). The result will be a name with only a-z, 0-9 and _.
                 *
                 * @method cleanName
                 * @static
                 * @param {String} s String to clean up.
                 * @return {String} Cleaned string.
                 */
                cleanName: function (name) {
                    var i, lookup;

                    // Replace diacritics
                    lookup = [
                        /[\300-\306]/g, 'A', /[\340-\346]/g, 'a',
                        /\307/g, 'C', /\347/g, 'c',
                        /[\310-\313]/g, 'E', /[\350-\353]/g, 'e',
                        /[\314-\317]/g, 'I', /[\354-\357]/g, 'i',
                        /\321/g, 'N', /\361/g, 'n',
                        /[\322-\330]/g, 'O', /[\362-\370]/g, 'o',
                        /[\331-\334]/g, 'U', /[\371-\374]/g, 'u'
                    ];

                    for (i = 0; i < lookup.length; i += 2) {
                        name = name.replace(lookup[i], lookup[i + 1]);
                    }

                    // Replace whitespace
                    name = name.replace(/\s+/g, '_');

                    // Remove anything else
                    name = name.replace(/[^a-z0-9_\-\.]+/gi, '');

                    return name;
                },

                /**
                 * Builds a full url out of a base URL and an object with items to append as query string items.
                 *
                 * @method buildUrl
                 * @static
                 * @param {String} url Base URL to append query string items to.
                 * @param {Object} items Name/value object to serialize as a querystring.
                 * @return {String} String with url + serialized query string items.
                 */
                buildUrl: function (url, items) {
                    var query = '';

                    u.Basic.each(items, function (value, name) {
                        query += (query ? '&' : '') + encodeURIComponent(name) + '=' + encodeURIComponent(value);
                    });

                    if (query) {
                        url += (url.indexOf('?') > 0 ? '&' : '?') + query;
                    }

                    return url;
                },

                /**
                 * Formats the specified number as a size string for example 1024 becomes 1 KB.
                 *
                 * @method formatSize
                 * @static
                 * @param {Number} size Size to format as string.
                 * @return {String} Formatted size string.
                 */
                formatSize: function (size) {
                    var self = this;

                    function round(num, precision) {
                        return Math.round(num * Math.pow(10, precision)) / Math.pow(10, precision);
                    }

                    size = parseInt(size, 10);
                    if (isNaN(size)) {
                        return self.translate('N/A');
                    }

                    var boundary = Math.pow(1024, 4);

                    // TB
                    if (size > boundary) {
                        return round(size / boundary, 1) + " " + self.translate('tb');
                    }

                    // GB
                    if (size > (boundary /= 1024)) {
                        return round(size / boundary, 1) + " " + self.translate('gb');
                    }

                    // MB
                    if (size > (boundary /= 1024)) {
                        return round(size / boundary, 1) + " " + self.translate('mb');
                    }

                    // KB
                    if (size > 1024) {
                        return Math.round(size / 1024) + " " + self.translate('kb');
                    }

                    return size + " " + self.translate('b');
                },

                /**
                 * @private
                 */
                mimes2extList: moxie.core.utils.Mime.mimes2extList,

                /**
                 Resolve url - among other things will turn relative url to absolute

                 @method resolveUrl
                 @static
                 @param {String|Object} url Either absolute or relative, or a result of parseUrl call
                 @return {String} Resolved, absolute url
                 */
                resolveUrl: u.Url.resolveUrl,

                /**
                 * Parses the specified size string into a byte value. For example 10kb becomes 10240.
                 *
                 * @method parseSize
                 * @static
                 * @param {String|Number} size String to parse or number to just pass through.
                 * @return {Number} Size in bytes.
                 */
                parseSize: u.Basic.parseSizeStr,

                delay: u.Basic.delay,


                /**
                 Parent object for all event dispatching components and objects

                 @class plupload.EventTarget
                 @private
                 @constructor
                 */
                EventTarget: moxie.core.EventTarget,

                /**
                 Common set of methods and properties for every runtime instance

                 @class plupload.Runtime
                 @private

                 @param {Object} options
                 @param {String} type Sanitized name of the runtime
                 @param {Object} [caps] Set of capabilities that differentiate specified runtime
                 @param {Object} [modeCaps] Set of capabilities that do require specific operational mode
                 @param {String} [preferredMode='browser'] Preferred operational mode to choose if no required capabilities were requested
                 */
                Runtime: moxie.runtime.Runtime,

                /**
                 Provides a convenient way to create cross-browser file-picker. Generates file selection dialog on click,
                 converts selected files to _File_ objects, to be used in conjunction with _Image_, preloaded in memory
                 with _FileReader_ or uploaded to a server through _XMLHttpRequest_.

                 @class plupload.FileInput
                 @private
                 @constructor
                 @extends EventTarget
                 @uses RuntimeClient
                 @param {Object|String|DOMElement} options If options is string or node, argument is considered as _browse\_button_.
                 @param {String|DOMElement} options.browse_button DOM Element to turn into file picker.
                 @param {Array} [options.accept] Array of mime types to accept. By default accepts all.
                 @param {String} [options.file='file'] Name of the file field (not the filename).
                 @param {Boolean} [options.multiple=false] Enable selection of multiple files.
                 @param {Boolean} [options.directory=false] Turn file input into the folder input (cannot be both at the same time).
                 @param {String|DOMElement} [options.container] DOM Element to use as a container for file-picker. Defaults to parentNode
                 for _browse\_button_.
                 @param {Object|String} [options.required_caps] Set of required capabilities, that chosen runtime must support.
                 */
                FileInput: moxie.file.FileInput,

                /**
                 Utility for preloading o.Blob/o.File objects in memory. By design closely follows [W3C FileReader](http://www.w3.org/TR/FileAPI/#dfn-filereader)
                 interface. Where possible uses native FileReader, where - not falls back to shims.

                 @class plupload.FileReader
                 @private
                 @constructor
                 @extends EventTarget
                 @uses RuntimeClient
                 */
                FileReader: moxie.file.FileReader
            };

        });

// Included from: src/core/Collection.js

        /**
         * Collection.js
         *
         * Copyright 2017, Ephox
         * Released under AGPLv3 License.
         *
         * License: http://www.plupload.com/license
         * Contributing: http://www.plupload.com/contributing
         */


        /**
         Helper collection class - in a way a mix of object and array

         @contsructor
         @class plupload.core.Collection
         @private
         */
        define('plupload/core/Collection', [
            'plupload'
        ], function (Basic) {

            var Collection = function () {
                var _registry = {};
                var _length = 0;
                var _last;


                plupload.extend(this, {

                    count: function () {
                        return _length;
                    },

                    hasKey: function (key) {
                        return _registry.hasOwnProperty(key)
                    },


                    get: function (key) {
                        return _registry[key];
                    },


                    first: function () {
                        for (var key in _registry) {
                            return _registry[key];
                        }
                    },


                    last: function () {
                        return _last;
                    },


                    toObject: function () {
                        return _registry;
                    },


                    add: function (key, obj) {
                        var self = this;

                        if (typeof(key) === 'object' && !obj) {
                            return plupload.each(key, function (obj, key) {
                                self.add(key, obj);
                            });
                        }

                        if (_registry.hasOwnProperty(key)) {
                            return self.update.apply(self, arguments);
                        }

                        _registry[key] = _last = obj;
                        _length++;
                    },


                    remove: function (key) {
                        if (this.hasKey(key)) {
                            var last = _registry[key];

                            delete _registry[key];
                            _length--;

                            // renew ref to the last added item if necessary
                            if (_last === last) {
                                _last = findLast();
                            }
                        }
                    },


                    extract: function (key) {
                        var item = this.get(key);
                        this.remove(key);
                        return item;
                    },


                    shift: function () {
                        var self = this,
                            first, key;

                        for (key in _registry) {
                            first = _registry[key];
                            self.remove(key);
                            return first;
                        }
                    },


                    update: function (key, obj) {
                        _registry[key] = obj;
                    },


                    each: function (cb) {
                        plupload.each(_registry, cb);
                    },


                    combineWith: function () {
                        var newCol = new Collection();

                        newCol.add(_registry);

                        plupload.each(arguments, function (col) {
                            if (col instanceof Collection) {
                                newCol.add(col.toObject());
                            }
                        });
                        return newCol;
                    },


                    clear: function () {
                        _registry = {};
                        _last = null;
                        _length = 0;
                    }
                });


                function findLast() {
                    var key;
                    for (key in _registry) {
                    }
                    return _registry[key];
                }

            };

            return Collection;
        });

// Included from: src/core/ArrCollection.js

        /**
         * ArrCollection.js
         *
         * Copyright 2017, Ephox
         * Released under AGPLv3 License.
         *
         * License: http://www.plupload.com/license
         * Contributing: http://www.plupload.com/contributing
         */


        /**
         @contsructor
         @class plupload.core.ArrCollection
         @private
         */
        define('plupload/core/ArrCollection', [
            'plupload'
        ], function (plupload) {

            var ArrCollection = function () {
                var _registry = [];

                plupload.extend(this, {

                    count: function () {
                        return _registry.length;
                    },

                    hasKey: function (key) {
                        return this.getIdx(key) > -1;
                    },


                    get: function (key) {
                        var idx = this.getIdx(key);
                        return idx > -1 ? _registry[idx] : null;
                    },

                    getIdx: function (key) {
                        for (var i = 0, length = _registry.length; i < length; i++) {
                            if (_registry[i].uid === key) {
                                return i;
                            }
                        }
                        return -1;
                    },

                    getByIdx: function (idx) {
                        return _registry[idx]
                    },

                    first: function () {
                        return _registry[0];
                    },

                    last: function () {
                        return _registry[_registry.length - 1];
                    },

                    add: function (obj) {
                        obj = arguments[1] || obj; // make it compatible with Collection.add()

                        var idx = this.getIdx(obj.uid);
                        if (idx > -1) {
                            _registry[idx] = obj;
                            return idx;
                        }

                        _registry.push(obj);
                        return _registry.length - 1;
                    },

                    remove: function (key) {
                        return !!this.extract(key);
                    },

                    splice: function (start, length) {
                        start = plupload.typeOf(start) === 'undefinded' ? 0 : Math.max(start, 0);
                        length = plupload.typeOf(length) !== 'undefinded' && start + length < _registry.length ? length : _registry.length - start;

                        return _registry.splice(start, length);
                    },

                    extract: function (key) {
                        var idx = this.getIdx(key);
                        if (idx > -1) {
                            return _registry.splice(idx, 1);
                        }
                        return null;
                    },

                    shift: function () {
                        return _registry.shift();
                    },

                    update: function (key, obj) {
                        var idx = this.getIdx(key);
                        if (idx > -1) {
                            _registry[idx] = obj;
                            return true;
                        }
                        return false;
                    },

                    each: function (cb) {
                        plupload.each(_registry, cb);
                    },

                    combineWith: function () {
                        return Array.prototype.concat.apply(this.toArray(), arguments);
                    },

                    sort: function (cb) {
                        _registry.sort(cb || function (a, b) {
                            return a.priority - b.priority;
                        });
                    },

                    clear: function () {
                        _registry = [];
                    },

                    toObject: function () {
                        var obj = {};
                        for (var i = 0, length = _registry.length; i < length; i++) {
                            obj[_registry[i].uid] = _registry[i];
                        }
                        return obj;
                    },

                    toArray: function () {
                        return Array.prototype.slice.call(_registry);
                    }
                });
            };

            return ArrCollection;
        });

// Included from: src/core/Optionable.js

        /**
         * Optionable.js
         *
         * Copyright 2017, Ephox
         * Released under AGPLv3 License.
         *
         * License: http://www.plupload.com/license
         * Contributing: http://www.plupload.com/contributing
         */


        /**
         @contsructor
         @class plupload.core.Optionable
         @private
         @since 3.0
         */
        define('plupload/core/Optionable', [
            'plupload'
        ], function (plupload) {
            var EventTarget = moxie.core.EventTarget;

            var dispatches = [
                /**
                 * Dispatched when option is being changed.
                 *
                 * @event OptionChanged
                 * @param {Object} event
                 * @param {String} name Name of the option being changed
                 * @param {Mixed} value
                 * @param {Mixed} oldValue
                 */
                'OptionChanged'
            ];

            return (function (Parent) {

                /**
                 * @class Optionable
                 * @constructor
                 * @extends EventTarget
                 */
                function Optionable() {
                    Parent.apply(this, arguments);

                    this._options = {};
                }

                plupload.inherit(Optionable, Parent);

                plupload.extend(Optionable.prototype, {
                    /**
                     * Set the value for the specified option(s).
                     *
                     * @method setOption
                     * @since 2.1
                     * @param {String|Object} option Name of the option to change or the set of key/value pairs
                     * @param {Mixed} [value] Value for the option (is ignored, if first argument is object)
                     * @param {Boolean} [mustBeDefined] if truthy, any option that is not in defaults will be ignored
                     */
                    setOption: function (option, value, mustBeDefined) {
                        var self = this;
                        var oldValue;

                        if (typeof(option) === 'object') {
                            mustBeDefined = value;
                            plupload.each(option, function (value, option) {
                                self.setOption(option, value, mustBeDefined);
                            });
                            return;
                        }

                        if (mustBeDefined && !self._options.hasOwnProperty(option)) {
                            return;
                        }

                        oldValue = plupload.clone(self._options[option]);

                        //! basically if an option is of type object extend it rather than replace
                        if (plupload.typeOf(value) === 'object' && plupload.typeOf(self._options[option]) === 'object') {
                            // having some options as objects was a bad idea, prefixes is the way
                            plupload.extend(self._options[option], value);
                        } else {
                            self._options[option] = value;
                        }

                        self.trigger('OptionChanged', option, value, oldValue);
                    },

                    /**
                     * Get the value for the specified option or the whole configuration, if not specified.
                     *
                     * @method getOption
                     * @since 2.1
                     * @param {String} [option] Name of the option to get
                     * @return {Mixed} Value for the option or the whole set
                     */
                    getOption: function (option) {
                        if (!option) {
                            return this._options;
                        }

                        var value = this._options[option];
                        if (plupload.inArray(plupload.typeOf(value), ['array', 'object']) > -1) {
                            return plupload.extendImmutable({}, value);
                        } else {
                            return value;
                        }
                    },


                    /**
                     * Set many options as once.
                     *
                     * @method setOptions
                     * @param {Object} options
                     * @param {Boolean} [mustBeDefined] if truthy, any option that is not in defaults will be ignored
                     */
                    setOptions: function (options, mustBeDefined) {
                        if (typeof(options) !== 'object') {
                            return;
                        }
                        this.setOption(options, mustBeDefined);
                    },


                    /**
                     Gets all options.

                     @method getOptions
                     @return {Object}
                     */
                    getOptions: function () {
                        return this.getOption();
                    }
                });

                return Optionable;

            }(EventTarget));

        });

// Included from: src/core/Queueable.js

        /**
         * Queueable.js
         *
         * Copyright 2017, Ephox
         * Released under AGPLv3 License.se.
         *
         * License: http://www.plupload.com/license
         * Contributing: http://www.plupload.com/contributing
         */


        /**
         Every queue item must have properties, implement methods and fire events defined in this class

         @contsructor
         @class plupload.core.Queueable
         @private
         @decorator
         @extends EventTarget
         */
        define('plupload/core/Queueable', [
            'plupload',
            'plupload/core/Optionable'
        ], function (plupload, Optionable) {

            var dispatches = [
                /**
                 * Dispatched every time the state of queue changes
                 *
                 * @event statechanged
                 * @param {Object} event
                 * @param {Number} state New state
                 * @param {Number} prevState Previous state
                 */
                'statechanged',


                /**
                 * Dispatched when the item is put on pending list
                 *
                 * @event queued
                 * @param {Object} event
                 */
                'queued',


                /**
                 * Dispatched as soon as activity starts
                 *
                 * @event started
                 * @param {Object} event
                 */
                'started',


                'paused',


                'resumed',


                'stopped',


                /**
                 * Dispatched as the activity progresses
                 *
                 * @event
                 * @param {Object} event
                 *      @param {Number} event.percent
                 *      @param {Number} [event.processed]
                 *      @param {Number} [event.total]
                 */
                'progress',


                'failed',


                'done',


                'processed',

                'destroy'
            ];


            return (function (Parent) {

                function Queueable() {
                    Parent.apply(this, arguments);

                    /**
                     Unique identifier
                     @property uid
                     @type {String}
                     */
                    this.uid = plupload.guid();

                    this.state = Queueable.IDLE;

                    this.processed = 0;

                    this.total = 0;

                    this.percent = 0;

                    this.retries = 0;

                    /**
                     * Can be 0-Infinity - item with higher priority will have well... higher priority
                     * @property [priority=0]
                     * @type {Number}
                     */
                    this.priority = 0;

                    this.startedTimestamp = 0;

                    /**
                     * Set when item becomes Queueable.DONE or Queueable.FAILED.
                     * Used to calculate proper processedPerSec for the queue stats.
                     * @property processedTimestamp
                     * @type {Number}
                     */
                    this.processedTimestamp = 0;

                    if (MXI_DEBUG) {
                        this.bind('StateChanged', function (e, state, oldState) {
                            var self = this;

                            var stateToString = function (code) {
                                switch (code) {
                                    case Queueable.IDLE:
                                        return 'IDLE';

                                    case Queueable.PROCESSING:
                                        return 'PROCESSING';

                                    case Queueable.PAUSED:
                                        return 'PAUSED';

                                    case Queueable.RESUMED:
                                        return 'RESUMED';

                                    case Queueable.DONE:
                                        return 'DONE';

                                    case Queueable.FAILED:
                                        return 'FAILED';

                                    case Queueable.DESTROYED:
                                        return 'DESTROYED';
                                }
                            };

                            var indent = function () {
                                switch (self.ctorName) {
                                    case 'File':
                                        return "\t".repeat(2);

                                    case 'QueueUpload':
                                    case 'QueueResize':
                                        return "\t";

                                    case 'FileUploader':
                                        return "\t".repeat(3);

                                    case 'ChunkUploader':
                                        return "\t".repeat(4);

                                    default:
                                        return "\t";
                                }
                            };

                            plupload.ua.log("StateChanged:" + indent() + self.ctorName + '::' + self.uid + ' (' + stateToString(oldState) + ' to ' + stateToString(state) + ')');
                        }, 999);
                    }
                }

                Queueable.IDLE = 1;
                Queueable.PROCESSING = 2;
                Queueable.PAUSED = 6;
                Queueable.RESUMED = 7;
                Queueable.DONE = 5;
                Queueable.FAILED = 4;
                Queueable.DESTROYED = 8;

                plupload.inherit(Queueable, Parent);

                plupload.extend(Queueable.prototype, {

                    start: function () {
                        var prevState = this.state;

                        if (this.state === Queueable.PROCESSING) {
                            return false;
                        }

                        if (!this.startedTimestamp) {
                            this.startedTimestamp = +new Date();
                        }

                        this.state = Queueable.PROCESSING;
                        this.trigger('statechanged', this.state, prevState);
                        this.trigger('started');

                        return true;
                    },


                    pause: function () {
                        var prevState = this.state;

                        if (plupload.inArray(this.state, [Queueable.IDLE, Queueable.RESUMED, Queueable.PROCESSING]) === -1) {
                            return false;
                        }

                        this.processed = this.percent = 0; // by default reset all progress
                        this.loaded = this.processed; // for backward compatibility

                        this.state = Queueable.PAUSED;
                        this.trigger('statechanged', this.state, prevState);
                        this.trigger('paused');
                        return true;
                    },


                    resume: function () {
                        var prevState = this.state;

                        if (this.state !== Queueable.PAUSED && this.state !== Queueable.RESUMED) {
                            return false;
                        }

                        this.state = Queueable.RESUMED;
                        this.trigger('statechanged', this.state, prevState);
                        this.trigger('resumed');
                        return true;
                    },


                    stop: function () {
                        var prevState = this.state;

                        if (this.state === Queueable.IDLE) {
                            return false;
                        }

                        this.processed = this.percent = 0;
                        this.loaded = this.processed; // for backward compatibility

                        this.startedTimestamp = 0;

                        this.state = Queueable.IDLE;
                        this.trigger('statechanged', this.state, prevState);
                        this.trigger('stopped');
                        return true;
                    },


                    done: function (result) {
                        var prevState = this.state;

                        if (this.state === Queueable.DONE) {
                            return false;
                        }

                        this.processed = this.total;
                        this.loaded = this.processed; // for backward compatibility
                        this.percent = 100;

                        this.processedTimestamp = +new Date();

                        this.state = Queueable.DONE;
                        this.trigger('statechanged', this.state, prevState);
                        this.trigger('done', result);
                        this.trigger('processed');
                        return true;
                    },


                    failed: function (result) {
                        var prevState = this.state;

                        if (this.state === Queueable.FAILED) {
                            return false;
                        }

                        this.processed = this.percent = 0; // reset the progress
                        this.loaded = this.processed; // for backward compatibility

                        this.processedTimestamp = +new Date();

                        this.state = Queueable.FAILED;
                        this.trigger('statechanged', this.state, prevState);
                        this.trigger('failed', result);
                        this.trigger('processed');
                        return true;
                    },


                    progress: function (processed, total) {
                        if (total) {
                            this.total = total; // is this even required?
                        }

                        this.processed = Math.min(processed, this.total);
                        this.loaded = this.processed; // for backward compatibility
                        this.percent = Math.ceil(this.processed / this.total * 100);

                        this.trigger({
                            type: 'progress',
                            loaded: this.processed,
                            total: this.total
                        });
                    },


                    destroy: function () {
                        var prevState = this.state;

                        if (this.state === Queueable.DESTROYED) {
                            return false; // already destroyed
                        }

                        this.state = Queueable.DESTROYED;
                        this.trigger('statechanged', this.state, prevState);
                        this.trigger('destroy');
                        this.unbindAll();
                        return true;
                    }

                });

                return Queueable;

            }(Optionable));
        });

// Included from: src/core/Stats.js

        /**
         @class plupload.core.Stats
         @constructor
         @private
         */
        define('plupload/core/Stats', [], function () {

            return function () {
                var self = this;

                /**
                 * Total queue file size.
                 *
                 * @property size
                 * @deprecated use total
                 * @type Number
                 */
                self.size = 0;

                /**
                 * Total size of the queue in units.
                 *
                 * @property total
                 * @since 3.0
                 * @type Number
                 */
                self.total = 0;

                /**
                 * Total bytes uploaded.
                 *
                 * @property loaded
                 * @type Number
                 */
                self.loaded = 0;


                /**
                 * Number of files uploaded successfully.
                 *
                 * @property uploaded
                 * @deprecated use done
                 * @type Number
                 */
                self.uploaded = 0;

                /**
                 * Number of items processed successfully.
                 *
                 * @property done
                 * @since 3.0
                 * @type Number
                 */
                self.done = 0;

                /**
                 * Number of failed items.
                 *
                 * @property failed
                 * @type Number
                 */
                self.failed = 0;

                /**
                 * Number of items yet to be processed.
                 *
                 * @property queued
                 * @type Number
                 */
                self.queued = 0;

                /**
                 * Number of items currently paused.
                 *
                 * @property paused
                 * @type Number
                 */
                self.paused = 0;

                /**
                 * Number of items being processed.
                 *
                 * @property processing
                 * @type Number
                 */
                self.processing = 0;


                /**
                 * Number of items being paused.
                 *
                 * @property paused
                 * @type Number
                 */
                self.paused = 0;

                /**
                 * Percent of processed units.
                 *
                 * @property percent
                 * @type Number
                 */
                self.percent = 0;

                /**
                 * Bytes processed per second.
                 *
                 * @property bytesPerSec
                 * @deprecated use processedPerSec
                 * @type Number
                 */
                self.bytesPerSec = 0;

                /**
                 * Units processed per second.
                 *
                 * @property processedPerSec
                 * @since 3.0
                 * @type Number
                 */
                self.processedPerSec = 0;

                /**
                 * Resets the progress to its initial values.
                 *
                 * @method reset
                 */
                self.reset = function () {
                    self.size = // deprecated
                        self.total =
                            self.loaded = // deprecated
                                self.processed =
                                    self.uploaded = // deprecated
                                        self.done =
                                            self.failed =
                                                self.queued =
                                                    self.processing =
                                                        self.paused =
                                                            self.percent =
                                                                self.bytesPerSec = // deprecated
                                                                    self.processedPerSec = 0;
                };
            };
        });

// Included from: src/core/Queue.js

        /**
         * Queue.js
         *
         * Copyright 2017, Ephox
         * Released under AGPLv3 License.
         *
         * License: http://www.plupload.com/license
         * Contributing: http://www.plupload.com/contributing
         */


        /**
         @contsructor
         @class plupload.core.Queue
         @private
         */
        define('plupload/core/Queue', [
            'plupload',
            'plupload/core/ArrCollection',
            'plupload/core/Queueable',
            'plupload/core/Stats'
        ], function (plupload, ArrCollection, Queueable, Stats) {

            var dispatches = [
                /**
                 * Dispatched as soon as activity starts
                 *
                 * @event started
                 * @param {Object} event
                 */
                'Started',


                /**
                 * Dispatched as activity progresses
                 *
                 * @event progress
                 * @param {Object} event
                 * @param {Number} processed
                 * @param {Number} total
                 * @param {plupload.core.Stats} stats
                 */
                'Progress',

                /**
                 * Dispatched when activity is paused
                 *
                 * @event paused
                 * @param {Object} event
                 */
                'Paused',

                /**
                 * Dispatched when there's no more items in processing
                 *
                 * @event done
                 * @param {Object} event
                 */
                'Done',

                /**
                 * Dispatched as soon as activity ends
                 *
                 * @event stopped
                 * @param {Object} event
                 */
                'Stopped',

                /**
                 * Dispatched when queue is destroyed
                 *
                 * @event destroy
                 * @param {Object} event
                 */
                'Destroy'
            ];

            /**
             * @class Queue
             * @constructor
             * @extends EventTarget
             */
            return (function (Parent) {
                plupload.inherit(Queue, Parent);


                function Queue(options) {
                    Parent.apply(this, arguments);

                    /**
                     @property _queue
                     @type {Collection}
                     @private
                     */
                    this._queue = new ArrCollection();


                    /**
                     @property stats
                     @type {Stats}
                     @readOnly
                     */
                    this.stats = new Stats();


                    this._options = plupload.extend({}, this._options, {
                        max_slots: 1,
                        max_retries: 0,
                        auto_start: false,
                        finish_active: false
                    }, options);
                }

                plupload.extend(Queue.prototype, {

                    /**
                     * Returns number of items in the queue
                     *
                     * @method count
                     * @returns {Number}
                     */
                    count: function () {
                        return this._queue.count();
                    },

                    /**
                     * Start the queue
                     *
                     * @method start
                     */
                    start: function () {
                        if (!Queue.parent.start.call(this)) {
                            return false;
                        }
                        return processNext.call(this);
                    },


                    pause: function () {
                        if (!Queue.parent.pause.call(this)) {
                            return false;
                        }

                        this.forEachItem(function (item) {
                            item.pause();
                        });
                    },

                    /**
                     * Stop the queue. If `finish_active=true` the queue will wait until active items are done, before
                     * stopping.
                     *
                     * @method stop
                     */
                    stop: function () {
                        if (!Queue.parent.stop.call(this) || this.getOption('finish_active')) {
                            return false;
                        }

                        if (this.isActive()) {
                            this.forEachItem(function (item) {
                                item.stop();
                            });
                        }
                    },


                    forEachItem: function (cb) {
                        this._queue.each(cb);
                    },


                    getItem: function (uid) {
                        return this._queue.get(uid);
                    },


                    /**
                     * Add instance of Queueable to the queue. If `auto_start=true` queue will start as well.
                     *
                     * @method addItem
                     * @param {Queueable} item
                     */
                    addItem: function (item) {
                        var self = this;

                        item.bind('Started', function () {
                            if (self.calcStats()) {
                                plupload.delay.call(self, processNext);
                            }
                        });

                        item.bind('Resumed', function () {
                            self.start();
                        });

                        item.bind('Paused', function () {
                            if (self.calcStats()) {
                                plupload.delay.call(self, function () {
                                    if (!processNext.call(self) && !self.stats.processing) {
                                        self.pause();
                                    }
                                });
                            }
                        });

                        item.bind('Processed Stopped', function () {
                            if (self.calcStats()) {
                                plupload.delay.call(self, function () {
                                    if (!processNext.call(self) && !this.isStopped() && !this.isActive()) {
                                        self.stop();
                                    }
                                });
                            }
                        });

                        item.bind('Progress', function () {
                            if (self.calcStats()) {
                                self.trigger('Progress', self.stats.processed, self.stats.total, self.stats);
                            }
                        });

                        item.bind('Failed', function () {
                            if (self.getOption('max_retries') && this.retries < self.getOption('max_retries')) {
                                this.stop();
                                this.retries++;
                            }
                        });

                        this._queue.add(item.uid, item);
                        this.calcStats();
                        item.trigger('Queued');

                        if (self.getOption('auto_start') || self.state === Queueable.PAUSED) {
                            plupload.delay.call(this, this.start);
                        }
                    },


                    /**
                     * Extracts item from the queue by its uid and returns it.
                     *
                     * @method extractItem
                     * @param {String} uid
                     * @return {Queueable} Item that was removed
                     */
                    extractItem: function (uid) {
                        var item = this._queue.get(uid);
                        if (item) {
                            this.stopItem(item.uid);
                            this._queue.remove(uid);
                            this.calcStats();
                        }
                        return item;
                    },

                    /**
                     * Removes item from the queue and destroys it
                     *
                     * @method removeItem
                     * @param {String} uid
                     * @returns {Boolean} Result of the operation
                     */
                    removeItem: function (uid) {
                        var item = this.extractItem(uid);
                        if (item) {
                            item.destroy();
                            return true;
                        }
                        return false;
                    },


                    stopItem: function (uid) {
                        var item = this._queue.get(uid);
                        if (item) {
                            return item.stop();
                        } else {
                            return false;
                        }
                    },


                    pauseItem: function (uid) {
                        var item = this._queue.get(uid);
                        if (item) {
                            return item.pause();
                        } else {
                            return false;
                        }
                    },


                    resumeItem: function (uid) {
                        var item = this._queue.get(uid);
                        if (item) {
                            plupload.delay.call(this, function () {
                                this.start(); // start() will know if it needs to restart the queue
                            });
                            return item.resume();
                        } else {
                            return false;
                        }
                    },


                    splice: function (start, length) {
                        return this._queue.splice(start, length);
                    },


                    isActive: function () {
                        return this.stats && (this.stats.processing || this.stats.paused);
                    },

                    isStopped: function () {
                        return this.state === Queueable.IDLE || this.state === Queueable.DESTROYED;
                    },


                    countSpareSlots: function () {
                        return Math.max(this.getOption('max_slots') - this.stats.processing, 0);
                    },


                    toArray: function () {
                        return this._queue.toArray();
                    },


                    clear: function () {
                        var self = this;

                        if (self.state !== Queueable.IDLE) {
                            // stop the active queue first
                            self.bindOnce('Stopped', function () {
                                self.clear();
                            });
                            return self.stop();
                        } else {
                            self._queue.clear();
                            self.stats.reset();
                        }
                    },


                    calcStats: function () {
                        var self = this;
                        var stats = self.stats;
                        var processed = 0;
                        var processedDuringThisSession = 0;

                        if (!stats) {
                            return false; // maybe queue is destroyed
                        }

                        stats.reset();

                        self.forEachItem(function (item) {
                            switch (item.state) {
                                case Queueable.DONE:
                                    stats.done++;
                                    stats.uploaded = stats.done; // for backward compatibility
                                    break;

                                case Queueable.FAILED:
                                    stats.failed++;
                                    break;

                                case Queueable.PROCESSING:
                                    stats.processing++;
                                    break;

                                case Queueable.PAUSED:
                                    stats.paused++;
                                    break;

                                default:
                                    stats.queued++;
                            }

                            processed += item.processed;

                            if (!item.processedTimestamp || item.processedTimestamp > self.startedTimestamp) {
                                processedDuringThisSession += processed;
                            }

                            stats.processedPerSec = Math.ceil(processedDuringThisSession / ((+new Date() - self.startedTimestamp || 1) / 1000.0));

                            stats.processed = processed;
                            stats.total += item.total;
                            if (stats.total) {
                                stats.percent = Math.ceil(stats.processed / stats.total * 100);
                            }
                        });

                        // enable properties inherited from Queueable

                        /* TODO: this is good but it currently conflicts with deprecated total property in Uploader
                self.processed = stats.processed;
                self.total = stats.total;
                */
                        self.percent = stats.percent;

                        // for backward compatibility
                        stats.loaded = stats.processed;
                        stats.size = stats.total;
                        stats.bytesPerSec = stats.processedPerSec;

                        return true;
                    },


                    destroy: function () {
                        var self = this;

                        if (self.state === Queueable.DESTROYED) {
                            return false; // already destroyed
                        }

                        if (self.state !== Queueable.IDLE) {
                            // stop the active queue first
                            self.bindOnce('Stopped', function () {
                                plupload.delay.call(self, self.destroy);
                            });
                            return self.stop();
                        } else {
                            self.clear();
                            Queue.parent.destroy.call(this);
                            self._queue = self.stats = null;
                        }
                        return true;
                    }
                });


                /**
                 * Returns another Queueable.IDLE or Queueable.RESUMED item, or null.
                 */
                function getNextIdleItem() {
                    var nextItem;
                    this.forEachItem(function (item) {
                        if (item.state === Queueable.IDLE || item.state === Queueable.RESUMED) {
                            nextItem = item;
                            return false;
                        }
                    });
                    return nextItem ? nextItem : null;
                }


                function processNext() {
                    var item;

                    if (this.state !== Queueable.PROCESSING && this.state !== Queueable.PAUSED) {
                        return false;
                    }

                    if (this.stats.processing < this.getOption('max_slots')) {
                        item = getNextIdleItem.call(this);
                        if (item) {
                            if (item.trigger('beforestart')) {
                                item.setOptions(this.getOptions());
                                return item.start();
                            } else {
                                item.pause();
                                // we need to call it sync, otherwise another thread may pick up the same file, while it is processed in beforestart handler
                                processNext.call(this);
                            }
                        }
                    }
                    return false;
                }

                return Queue;

            }(Queueable));
        });

// Included from: src/QueueUpload.js

        /**
         * QueueUpload.js
         *
         * Copyright 2017, Ephox
         * Released under AGPLv3 License.
         *
         * License: http://www.plupload.com/license
         * Contributing: http://www.plupload.com/contributing
         */


        /**
         @class plupload.QueueUpload
         @extends plupload.core.Queue
         @constructor
         @private
         @final
         @since 3.0
         @param {Object} options
         */
        define('plupload/QueueUpload', [
            'plupload',
            'plupload/core/Queue'
        ], function (plupload, Queue) {

            return (function (Parent) {
                plupload.inherit(QueueUpload, Parent);

                function QueueUpload(options) {

                    Queue.call(this, {
                        max_slots: 1,
                        max_retries: 0,
                        auto_start: false,
                        finish_active: false,
                        url: false,
                        chunk_size: 0,
                        multipart: true,
                        http_method: 'POST',
                        params: {},
                        headers: false,
                        file_data_name: 'file',
                        send_file_name: true,
                        stop_on_fail: true
                    });

                    this.setOption = function (option, value) {
                        if (typeof(option) !== 'object') {
                            if (option == 'max_upload_slots') {
                                option = 'max_slots';
                            }
                        }
                        QueueUpload.prototype.setOption.call(this, option, value, true);
                    };

                    this.setOptions(options);
                }

                return QueueUpload;
            }(Queue));
        });

// Included from: src/QueueResize.js

        /**
         * QueueResize.js
         *
         * Copyright 2017, Ephox
         * Released under AGPLv3 License.
         *
         * License: http://www.plupload.com/license
         * Contributing: http://www.plupload.com/contributing
         */


        /**
         @class plupload.QueueResize
         @extends plupload.core.Queue
         @constructor
         @private
         @final
         @since 3.0
         @param {Object} options
         */
        define('plupload/QueueResize', [
            'plupload',
            'plupload/core/Queue'
        ], function (plupload, Queue) {

            return (function (Parent) {
                plupload.inherit(QueueResize, Parent);

                function QueueResize(options) {

                    Queue.call(this, {
                        max_slots: 1,
                        max_retries: 0,
                        auto_start: false,
                        finish_active: false,
                        resize: {}
                    });

                    this.setOption = function (option, value) {
                        if (typeof(option) !== 'object') {
                            if (option == 'max_resize_slots') {
                                option = 'max_slots';
                            }
                        }
                        QueueResize.prototype.setOption.call(this, option, value, true);
                    };


                    this.setOptions(options);
                }


                return QueueResize;
            }(Queue));
        });

// Included from: src/ChunkUploader.js

        /**
         * ChunkUploader.js
         *
         * Copyright 2017, Ephox
         * Released under AGPLv3 License.
         *
         * License: http://www.plupload.com/license
         * Contributing: http://www.plupload.com/contributing
         */

        /**
         * @class plupload.ChunkUploader
         * @extends plupload.core.Queueable
         * @constructor
         * @private
         * @final
         * @constructor
         */
        define('plupload/ChunkUploader', [
            'plupload',
            'plupload/core/Collection',
            'plupload/core/Queueable'
        ], function (plupload, Collection, Queueable) {
            var XMLHttpRequest = moxie.xhr.XMLHttpRequest;
            var FormData = moxie.xhr.FormData;

            function ChunkUploader(blob) {
                var _xhr;

                Queueable.call(this);

                this._options = {
                    file_data_name: 'file',
                    headers: false,
                    http_method: 'POST',
                    multipart: true,
                    params: {},
                    send_file_name: true,
                    url: false
                };

                plupload.extend(this, {

                    start: function () {
                        var self = this;
                        var url;
                        var formData;
                        var prevState = this.state;
                        var options = self._options;

                        if (this.state === Queueable.PROCESSING) {
                            return false;
                        }

                        if (!this.startedTimestamp) {
                            this.startedTimestamp = +new Date();
                        }

                        this.state = Queueable.PROCESSING;
                        this.trigger('statechanged', this.state, prevState);

                        _xhr = new XMLHttpRequest();

                        if (_xhr.upload) {
                            _xhr.upload.onprogress = function (e) {
                                self.progress(e.loaded, e.total);
                            };
                        }

                        _xhr.onload = function () {
                            var result = {
                                response: this.responseText,
                                status: this.status,
                                responseHeaders: this.getAllResponseHeaders()
                            };

                            if (this.status < 200 || this.status >= 400) { // assume error
                                return self.failed(result);
                            }

                            self.done(result);
                        };

                        _xhr.onerror = function () {
                            self.failed(); // TODO: reason here
                        };

                        _xhr.onloadend = function () {
                            // we do not need _xhr anymore, so destroy it
                            setTimeout(function () { // we detach to sustain reference until all handlers are done
                                if (_xhr) {
                                    _xhr.destroy();
                                    _xhr = null;
                                }
                            }, 1);
                        };

                        try {
                            url = options.multipart ? options.url : buildUrl(options.url, options.params);
                            _xhr.open(options.http_method, url, true);


                            // headers must be set after request is already opened, otherwise INVALID_STATE_ERR exception will raise
                            if (!plupload.isEmptyObj(options.headers)) {
                                plupload.each(options.headers, function (val, key) {
                                    _xhr.setRequestHeader(key, val);
                                });
                            }


                            if (options.multipart) {
                                formData = new FormData();

                                if (!plupload.isEmptyObj(options.params)) {
                                    plupload.each(options.params, function (val, key) {
                                        formData.append(key, val);
                                    });
                                }

                                formData.append(options.file_data_name, blob);

                                _xhr.send(formData);
                            } else { // if no multipart, send as binary stream
                                if (plupload.isEmptyObj(options.headers) || !_xhr.hasRequestHeader('content-type')) {
                                    _xhr.setRequestHeader('content-type', 'application/octet-stream'); // binary stream header
                                }

                                _xhr.send(blob);
                            }

                            this.trigger('started');
                        } catch (ex) {
                            self.failed();
                        }
                    },


                    stop: function () {
                        if (_xhr) {
                            _xhr.abort();
                            _xhr.destroy();
                            _xhr = null;
                        }
                        ChunkUploader.prototype.stop.call(this);
                    },

                    setOption: function (option, value) {
                        ChunkUploader.prototype.setOption.call(this, option, value, true);
                    },

                    setOptions: function (options) {
                        ChunkUploader.prototype.setOption.call(this, options, true);
                    },

                    destroy: function () {
                        this.stop();
                        ChunkUploader.prototype.destroy.call(this);
                    }
                });


                /**
                 * Builds a full url out of a base URL and an object with items to append as query string items.
                 *
                 * @method buildUrl
                 * @private
                 * @param {String} url Base URL to append query string items to.
                 * @param {Object} items Name/value object to serialize as a querystring.
                 * @return {String} String with url + serialized query string items.
                 */
                function buildUrl(url, items) {
                    var query = '';

                    plupload.each(items, function (value, name) {
                        query += (query ? '&' : '') + encodeURIComponent(name) + '=' + encodeURIComponent(value);
                    });

                    if (query) {
                        url += (url.indexOf('?') > 0 ? '&' : '?') + query;
                    }

                    return url;
                }

            }

            plupload.inherit(ChunkUploader, Queueable);

            return ChunkUploader;
        });

// Included from: src/FileUploader.js

        /**
         * FileUploader.js
         *
         * Copyright 2017, Ephox
         * Released under AGPLv3 License.se.
         *
         * License: http://www.plupload.com/license
         * Contributing: http://www.plupload.com/contributing
         */

        /**
         * @class plupload.FileUploader
         * @extends plupload.core.Queueable
         * @constructor
         * @since 3.0
         * @final
         */
        define('plupload/FileUploader', [
            'plupload',
            'plupload/core/Collection',
            'plupload/core/Queueable',
            'plupload/ChunkUploader'
        ], function (plupload, Collection, Queueable, ChunkUploader) {


            function FileUploader(file, queue) {
                var _chunks = new Collection();
                var _totalChunks = 1;

                Queueable.call(this);

                this._options = {
                    chunk_size: 0,
                    params: {},
                    send_file_name: true,
                    stop_on_fail: true
                };

                plupload.extend(this, {
                    /**
                     When send_file_name is set to true, will be sent with the request as `name` param.
                     Can be used on server-side to override original file name.

                     @property name
                     @type {String}
                     */
                    name: file.name,


                    start: function () {
                        var self = this;
                        var prevState = this.state;
                        var up;

                        if (this.state === Queueable.PROCESSING) {
                            return false;
                        }

                        if (!this.startedTimestamp) {
                            this.startedTimestamp = +new Date();
                        }

                        this.state = Queueable.PROCESSING;
                        this.trigger('statechanged', this.state, prevState);

                        // send additional 'name' parameter only if required or explicitly requested
                        if (self._options.send_file_name) {
                            self._options.params.name = self.target_name || self.name;
                        }

                        if (self._options.chunk_size) {
                            _totalChunks = Math.ceil(file.size / self._options.chunk_size);
                            self.uploadChunk(false, true);
                        } else {
                            up = new ChunkUploader(file);

                            up.bind('progress', function (e) {
                                self.progress(e.loaded, e.total);
                            });

                            up.bind('done', function (e, result) {
                                self.done(result);
                            });

                            up.bind('failed', function (e, result) {
                                self.failed(result);
                            });

                            up.setOptions(self._options);

                            queue.addItem(up);
                        }

                        this.trigger('started');
                    },


                    uploadChunk: function (seq, dontStop) {
                        var self = this;
                        var chunkSize = this.getOption('chunk_size');
                        var up;
                        var chunk = {};
                        var _options;

                        chunk.seq = parseInt(seq, 10) || getNextChunk();
                        chunk.start = chunk.seq * chunkSize;
                        chunk.end = Math.min(chunk.start + chunkSize, file.size);
                        chunk.total = file.size;

                        // do not proceed for weird chunks
                        if (chunk.start < 0 || chunk.start >= file.size) {
                            return false;
                        }

                        _options = plupload.extendImmutable({}, this.getOptions(), {
                            params: {
                                chunk: chunk.seq,
                                chunks: _totalChunks
                            }
                        });

                        up = new ChunkUploader(file.slice(chunk.start, chunk.end, file.type));

                        up.bind('progress', function (e) {
                            self.progress(calcProcessed() + e.loaded, file.size);
                        });

                        up.bind('failed', function (e, result) {
                            _chunks.add(chunk.seq, plupload.extend({
                                state: Queueable.FAILED
                            }, chunk));

                            self.trigger('chunkuploadfailed', plupload.extendImmutable({}, chunk, result));

                            if (_options.stop_on_fail) {
                                self.failed(result);
                            }
                        });

                        up.bind('done', function (e, result) {
                            _chunks.add(chunk.seq, plupload.extend({
                                state: Queueable.DONE
                            }, chunk));

                            self.trigger('chunkuploaded', plupload.extendImmutable({}, chunk, result));

                            if (calcProcessed() >= file.size) {
                                self.progress(file.size, file.size);
                                self.done(result); // obviously we are done
                            } else if (dontStop) {
                                plupload.delay(function () {
                                    self.uploadChunk(getNextChunk(), dontStop);
                                });
                            }
                        });

                        up.bind('processed', function () {
                            this.destroy();
                        });

                        up.setOptions(_options);

                        _chunks.add(chunk.seq, plupload.extend({
                            state: Queueable.PROCESSING
                        }, chunk));

                        queue.addItem(up);

                        // enqueue even more chunks if slots available
                        if (dontStop && queue.countSpareSlots()) {
                            self.uploadChunk(getNextChunk(), dontStop);
                        }

                        return true;
                    },

                    destroy: function () {
                        FileUploader.prototype.destroy.call(this);
                        _chunks.clear();
                    }
                });


                function calcProcessed() {
                    var processed = 0;

                    _chunks.each(function (item) {
                        if (item.state === Queueable.DONE) {
                            processed += (item.end - item.start);
                        }
                    });

                    return processed;
                }


                function getNextChunk() {
                    var i = 0;
                    while (i < _totalChunks && _chunks.hasKey(i)) {
                        i++;
                    }
                    return i;
                }

            }


            plupload.inherit(FileUploader, Queueable);

            return FileUploader;
        });

// Included from: src/ImageResizer.js

        /**
         * ImageResizer.js
         *
         * Copyright 2017, Ephox
         * Released under AGPLv3 License.
         *
         * License: http://www.plupload.com/license
         * Contributing: http://www.plupload.com/contributing
         */

        /**
         @class plupload.ImageResizer
         @extends plupload.core.Queueable
         @constructor
         @private
         @final
         @since 3.0
         @param {plupload.File} fileRef
         */
        define("plupload/ImageResizer", [
            'plupload',
            'plupload/core/Queueable'
        ], function (plupload, Queueable) {
            var mxiImage = moxie.image.Image;

            function ImageResizer(fileRef) {

                Queueable.call(this);

                this._options = {
                    type: 'image/jpeg',
                    quality: 90,
                    crop: false,
                    fit: true,
                    preserveHeaders: true,
                    resample: 'default',
                    multipass: true
                };

                this.setOption = function (option, value) {
                    if (typeof(option) !== 'object' && !this._options.hasOwnProperty(option)) {
                        return;
                    }
                    ImageResizer.prototype.setOption.apply(this, arguments);
                };


                this.start = function (options) {
                    var self = this;
                    var img;

                    if (options) {
                        this.setOptions(options.resize);
                    }

                    img = new mxiImage();

                    img.bind('load', function () {
                        this.resize(self.getOptions());
                    });

                    img.bind('resize', function () {
                        self.done(this.getAsBlob(self.getOption('type'), self.getOption('quality')));
                        this.destroy();
                    });

                    img.bind('error', function () {
                        self.failed();
                        this.destroy();
                    });

                    img.load(fileRef, self.getOption('runtimeOptions'));
                };
            }

            plupload.inherit(ImageResizer, Queueable);

            // ImageResizer is only included for builds with Image manipulation support, so we add plupload.Image here manually
            plupload.Image = mxiImage;

            return ImageResizer;
        });

// Included from: src/File.js

        /**
         * File.js
         *
         * Copyright 2017, Ephox
         * Released under AGPLv3 License.se.
         *
         * License: http://www.plupload.com/license
         * Contributing: http://www.plupload.com/contributing
         */

        /**
         * @class plupload.File
         * @extends plupload.core.Queueable
         * @constructor
         * @since 3.0
         * @final
         */
        define('plupload/File', [
            'plupload',
            'plupload/core/Queueable',
            'plupload/FileUploader',
            'plupload/ImageResizer'
        ], function (plupload, Queueable, FileUploader, ImageResizer) {

            function File(file, queueUpload, queueResize) {
                Queueable.call(this);


                plupload.extend(this, {
                    /**
                     * For backward compatibility
                     *
                     * @property id
                     * @type {String}
                     * @deprecated
                     */
                    id: this.uid,


                    /**
                     When send_file_name is set to true, will be sent with the request as `name` param.
                     Can be used on server-side to override original file name.

                     @property name
                     @type {String}
                     */
                    name: file.name,

                    /**
                     @property target_name
                     @type {String}
                     @deprecated use name
                     */
                    target_name: null,

                    /**
                     * File type, `e.g image/jpeg`
                     *
                     * @property type
                     * @type String
                     */
                    type: file.type,

                    /**
                     * File size in bytes (may change after client-side manupilation).
                     *
                     * @property size
                     * @type Number
                     */
                    size: file.size,

                    /**
                     * Original file size in bytes.
                     *
                     * @property origSize
                     * @type Number
                     */
                    origSize: file.size,

                    start: function () {
                        var prevState = this.state;

                        if (this.state === Queueable.PROCESSING) {
                            return false;
                        }

                        this.state = Queueable.PROCESSING;
                        this.trigger('statechanged', this.state, prevState);
                        this.trigger('started');

                        if (!plupload.isEmptyObj(this._options.resize) && isImage(this.type) && runtimeCan(file, 'send_binary_string')) {
                            this.resizeAndUpload();
                        } else {
                            this.upload();
                        }
                        return true;
                    },

                    /**
                     * Get the file for which this File is responsible
                     *
                     * @method getSource
                     * @returns {moxie.file.File}
                     */
                    getSource: function () {
                        return file;
                    },

                    /**
                     * Returns file representation of the current runtime. For HTML5 runtime
                     * this is going to be native browser File object
                     * (for backward compatibility)
                     *
                     * @method getNative
                     * @deprecated
                     * @returns {File|Blob|Object}
                     */
                    getNative: function () {
                        return this.getFile().getSource();
                    },


                    resizeAndUpload: function () {
                        var self = this;
                        var opts = self.getOptions();
                        var rszr = new ImageResizer(file);

                        rszr.bind('progress', function (e) {
                            self.progress(e.loaded, e.total);
                        });

                        rszr.bind('done', function (e, file) {
                            file = file;
                            self.upload();
                        });

                        rszr.bind('failed', function () {
                            self.upload();
                        });

                        rszr.setOption('runtimeOptions', {
                            runtime_order: opts.runtimes,
                            required_caps: opts.required_features,
                            preferred_caps: opts.preferred_caps,
                            swf_url: opts.flash_swf_url,
                            xap_url: opts.silverlight_xap_url
                        });

                        queueResize.addItem(rszr);
                    },


                    upload: function () {
                        var self = this;
                        var up = new FileUploader(file, queueUpload);

                        up.bind('paused', function () {
                            self.pause();
                        });

                        up.bind('resumed', function () {
                            this.start();
                        });

                        up.bind('started', function () {
                            self.trigger('startupload');
                        });

                        up.bind('progress', function (e) {
                            self.progress(e.loaded, e.total);
                        });

                        up.bind('done', function (e, result) {
                            self.done(result);
                        });

                        up.bind('failed', function (e, result) {
                            self.failed(result);
                        });

                        up.setOptions(self.getOptions());

                        up.start();
                    },


                    destroy: function () {
                        File.prototype.destroy.call(this);
                        file = null;
                    }
                });
            }


            function isImage(type) {
                return plupload.inArray(type, ['image/jpeg', 'image/png']) > -1;
            }


            function runtimeCan(blob, cap) {
                if (blob.ruid) {
                    var info = plupload.Runtime.getInfo(blob.ruid);
                    if (info) {
                        return info.can(cap);
                    }
                }
                return false;
            }


            plupload.inherit(File, Queueable);

            return File;
        });

// Included from: src/Uploader.js

        /**
         * Uploader.js
         *
         * Copyright 2017, Ephox
         * Released under AGPLv3 License.
         *
         * License: http://www.plupload.com/license
         * Contributing: http://www.plupload.com/contributing
         */


        /**
         @class plupload.Uploader
         @extends plupload.core.Queue
         @constructor
         @public
         @final

         @param {Object} settings For detailed information about each option check documentation.
         @param {String|DOMElement} settings.browse_button id of the DOM element or DOM element itself to use as file dialog trigger.
         @param {Number|String} [settings.chunk_size=0] Chunk size in bytes to slice the file into. Shorcuts with b, kb, mb, gb, tb suffixes also supported. `e.g. 204800 or "204800b" or "200kb"`. By default - disabled.
         @param {Boolean} [settings.send_chunk_number=true] Whether to send chunks and chunk numbers, or total and offset bytes.
         @param {String|DOMElement} [settings.container] id of the DOM element or DOM element itself that will be used to wrap uploader structures. Defaults to immediate parent of the `browse_button` element.
         @param {String|DOMElement} [settings.drop_element] id of the DOM element or DOM element itself to use as a drop zone for Drag-n-Drop.
         @param {String} [settings.file_data_name="file"] Name for the file field in Multipart formated message.
         @param {Object} [settings.filters={}] Set of file type filters.
         @param {Array} [settings.filters.mime_types=[]] List of file types to accept, each one defined by title and list of extensions. `e.g. {title : "Image files", extensions : "jpg,jpeg,gif,png"}`. Dispatches `plupload.FILE_EXTENSION_ERROR`
         @param {String|Number} [settings.filters.max_file_size=0] Maximum file size that the user can pick, in bytes. Optionally supports b, kb, mb, gb, tb suffixes. `e.g. "10mb" or "1gb"`. By default - not set. Dispatches `plupload.FILE_SIZE_ERROR`.
         @param {Boolean} [settings.filters.prevent_duplicates=false] Do not let duplicates into the queue. Dispatches `plupload.FILE_DUPLICATE_ERROR`.
         @param {String} [settings.flash_swf_url] URL of the Flash swf.
         @param {Object} [settings.headers] Custom headers to send with the upload. Hash of name/value pairs.
         @param {String} [settings.http_method="POST"] HTTP method to use during upload (only PUT or POST allowed).
         @param {Number} [settings.max_retries=0] How many times to retry the chunk or file, before triggering Error event.
         @param {Boolean} [settings.multipart=true] Whether to send file and additional parameters as Multipart formated message.
         @param {Boolean} [settings.multi_selection=true] Enable ability to select multiple files at once in file dialog.
         @param {Object} [settings.params] Hash of key/value pairs to send with every file upload.
         @param {String|Object} [settings.required_features] Either comma-separated list or hash of required features that chosen runtime should absolutely possess.
         @param {Object} [settings.resize] Enable resizing of images on client-side. Applies to `image/jpeg` and `image/png` only. `e.g. {width : 200, height : 200, quality : 90, crop: true}`
         @param {Number} settings.resize.width Resulting width
         @param {Number} [settings.resize.height=width] Resulting height (optional, if not supplied will default to width)
         @param {String} [settings.resize.type='image/jpeg'] MIME type of the resulting image
         @param {Number} [settings.resize.quality=90] In the case of JPEG, controls the quality of resulting image
         @param {Boolean} [settings.resize.crop='cc'] If not falsy, image will be cropped, by default from center
         @param {Boolean} [settings.resize.fit=true] In case of crop whether to upscale the image to fit the exact dimensions
         @param {Boolean} [settings.resize.preserveHeaders=true] Whether to preserve meta headers (on JPEGs after resize)
         @param {String} [settings.resize.resample='default'] Resampling algorithm to use during resize
         @param {Boolean} [settings.resize.multipass=true] Whether to scale the image in steps (results in better quality)
         @param {String} [settings.runtimes="html5,flash,silverlight,html4"] Comma separated list of runtimes, that Plupload will try in turn, moving to the next if previous fails.
         @param {Boolean} [settings.send_file_name=true] Whether to send file name as additional argument - 'name' (required for chunked uploads and some other cases where file name cannot be sent via normal ways).
         @param {String} [settings.silverlight_xap_url] URL of the Silverlight xap.
         @param {Boolean} [settings.unique_names=false] If true will generate unique filenames for uploaded files.
         @param {String} settings.url URL of the server-side upload handler.
         */

        /**
         Fires when the current RunTime has been initialized.

         @event Init
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         */

        /**
         Fires after the init event incase you need to perform actions there.

         @event PostInit
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         */

        /**
         Fires when the option is changed in via uploader.setOption().

         @event OptionChanged
         @since 2.1
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         @param {String} name Name of the option that was changed
         @param {Mixed} value New value for the specified option
         @param {Mixed} oldValue Previous value of the option
         */

        /**
         Fires when the silverlight/flash or other shim needs to move.

         @event Refresh
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         */

        /**
         Fires when the overall state is being changed for the upload queue.

         @event StateChanged
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         */

        /**
         Fires when browse_button is clicked and browse dialog shows.

         @event Browse
         @since 2.1.2
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         */

        /**
         Fires for every filtered file before it is added to the queue.

         @event FileFiltered
         @since 2.1
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         @param {plupload.File} file Another file that has to be added to the queue.
         */

        /**
         Fires when the file queue is changed. In other words when files are added/removed to the files array of the uploader instance.

         @event QueueChanged
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         */

        /**
         Fires after files were filtered and added to the queue.

         @event FilesAdded
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         @param {Array} files Array of FileUploader objects that were added to the queue by user.
         */

        /**
         Fires when file is removed from the queue.

         @event FilesRemoved
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         @param {Array} files Array of files that got removed.
         */

        /**
         Fires just before a file is uploaded. Can be used to cancel upload of the current file
         by returning false from the handler.

         @event BeforeUpload
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         @param {plupload.File} file File to be uploaded.
         */

        /**
         Fires when a file is to be uploaded by the runtime.

         @event UploadFile
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         @param {plupload.File} file File to be uploaded.
         */

        /**
         Fires while a file is being uploaded. Use this event to update the current file upload progress.

         @event UploadProgress
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         @param {plupload.File} file File that is currently being uploaded.
         */

        /**
         Fires when file chunk is uploaded.

         @event ChunkUploaded
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         @param {plupload.File} file File that the chunk was uploaded for.
         @param {Object} result Object with response properties.
         @param {Number} result.offset The amount of bytes the server has received so far, including this chunk.
         @param {Number} result.total The size of the file.
         @param {String} result.response The response body sent by the server.
         @param {Number} result.status The HTTP status code sent by the server.
         @param {String} result.responseHeaders All the response headers as a single string.
         */

        /**
         Fires when a file is successfully uploaded.

         @event FileUploaded
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         @param {plupload.File} file File that was uploaded.
         @param {Object} result Object with response properties.
         @param {String} result.response The response body sent by the server.
         @param {Number} result.status The HTTP status code sent by the server.
         @param {String} result.responseHeaders All the response headers as a single string.
         */

        /**
         Fires when all files in a queue are uploaded

         @event UploadComplete
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         */


        /**
         Fires whenever upload is aborted for some reason

         @event CancelUpload
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         */

        /**
         Fires when a error occurs.

         @event Error
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         @param {Object} error Contains code, message and sometimes file and other details.
         @param {Number} error.code The plupload error code.
         @param {String} error.message Description of the error (uses i18n).
         */

        /**
         Fires when destroy method is called.

         @event Destroy
         @param {plupload.Uploader} uploader Uploader instance sending the event.
         */
        define('plupload/Uploader', [
            'plupload',
            'plupload/core/Collection',
            'plupload/core/Queue',
            'plupload/QueueUpload',
            'plupload/QueueResize',
            'plupload/File'
        ], function (plupload, Collection, Queue, QueueUpload, QueueResize, PluploadFile) {

            var fileFilters = {};
            var undef;


            function Uploader(options) {
                var _fileInputs = [];
                var _fileDrops = [];
                var _queueUpload, _queueResize;
                var _initialized = false;
                var _disabled = false;

                var _options = normalizeOptions(plupload.extend({
                    backward_compatibility: true,
                    chunk_size: 0,
                    file_data_name: 'file',
                    filters: {
                        mime_types: '*',
                        prevent_duplicates: false,
                        max_file_size: 0
                    },
                    flash_swf_url: 'js/Moxie.swf',
                    // @since 2.3
                    http_method: 'POST',
                    // headers: false, // Plupload had a required feature with the same name, comment it to avoid confusion
                    max_resize_slots: 1,
                    max_retries: 0,
                    max_upload_slots: 1,
                    multipart: true,
                    multipart_params: {}, // deprecated, use - params,
                    multi_selection: true,
                    // @since 3
                    params: {},
                    resize: false,
                    runtimes: plupload.Runtime.order,
                    send_chunk_number: true, // whether to send chunks and chunk numbers, instead of total and offset bytes
                    send_file_name: true,
                    silverlight_xap_url: 'js/Moxie.xap',

                    // during normalization, these should be processed last
                    required_features: false,
                    preferred_caps: false
                }, options));

                Queue.call(this);


                // Add public methods
                plupload.extend(this, {

                    _options: _options,

                    /**
                     * Unique id for the Uploader instance.
                     *
                     * @property id
                     * @type String
                     */
                    id: this.uid,

                    /**
                     * Current state of the total uploading progress. This one can either be plupload.STARTED or plupload.STOPPED.
                     * These states are controlled by the stop/start methods. The default value is STOPPED.
                     *
                     * @property state
                     * @type Number
                     */
                    state: plupload.STOPPED,

                    /**
                     * Map of features that are available for the uploader runtime. Features will be filled
                     * before the init event is called, these features can then be used to alter the UI for the end user.
                     * Some of the current features that might be in this map is: dragdrop, chunks, jpgresize, pngresize.
                     *
                     * @property features
                     * @type Object
                     * @deprecated
                     */
                    features: {},

                    /**
                     * Object with name/value settings.
                     *
                     * @property settings
                     * @type Object
                     * @deprecated Use `getOption()/setOption()`
                     */
                    settings: _options,

                    /**
                     * Current runtime name
                     *
                     * @property runtime
                     * @type String
                     * @deprecated There might be multiple runtimes per uploader
                     */
                    runtime: null,

                    /**
                     * Current upload queue, an array of File instances
                     *
                     * @property files
                     * @deprecated use forEachItem(callback) to cycle over the items in the queue
                     * @type Array
                     */
                    files: [],

                    /**
                     * Total progess information. How many files has been uploaded, total percent etc.
                     *
                     * @property total
                     * @deprecated use stats
                     */
                    total: this.stats,

                    /**
                     * Initializes the Uploader instance and adds internal event listeners.
                     *
                     * @method init
                     */
                    init: function () {
                        var self = this, preinitOpt, err;

                        preinitOpt = self.getOption('preinit');
                        if (typeof(preinitOpt) == "function") {
                            preinitOpt(self);
                        } else {
                            plupload.each(preinitOpt, function (func, name) {
                                self.bind(name, func);
                            });
                        }

                        bindEventListeners.call(self);

                        // Check for required options
                        plupload.each(['container', 'browse_button', 'drop_element'], function (el) {
                            if (self.getOption(el) === null) {
                                err = {
                                    code: plupload.INIT_ERROR,
                                    message: plupload.sprintf(plupload.translate("%s specified, but cannot be found."), el)
                                }
                                return false;
                            }
                        });

                        if (err) {
                            return self.trigger('Error', err);
                        }


                        if (!self.getOption('browse_button') && !self.getOption('drop_element')) {
                            return self.trigger('Error', {
                                code: plupload.INIT_ERROR,
                                message: plupload.translate("You must specify either browse_button or drop_element.")
                            });
                        }


                        initControls.call(self, function (initialized) {
                            var runtime;
                            var initOpt = self.getOption('init');
                            var queueOpts = plupload.extendImmutable({}, self.getOption(), {auto_start: true});

                            if (typeof(initOpt) == "function") {
                                initOpt(self);
                            } else {
                                plupload.each(initOpt, function (func, name) {
                                    self.bind(name, func);
                                });
                            }

                            if (initialized) {
                                _initialized = true;
                                runtime = plupload.Runtime.getInfo(getRUID());

                                _queueUpload = new QueueUpload(queueOpts);
                                _queueResize = new QueueResize(queueOpts);

                                self.trigger('Init', {
                                    ruid: runtime.uid,
                                    runtime: self.runtime = runtime.type
                                });

                                self.trigger('PostInit');
                            } else {
                                self.trigger('Error', {
                                    code: plupload.INIT_ERROR,
                                    message: plupload.translate('Init error.')
                                });
                            }
                        });
                    },

                    /**
                     * Set the value for the specified option(s).
                     *
                     * @method setOption
                     * @since 2.1
                     * @param {String|Object} option Name of the option to change or the set of key/value pairs
                     * @param {Mixed} [value] Value for the option (is ignored, if first argument is object)
                     */
                    setOption: function (option, value) {
                        if (_initialized) {
                            // following options cannot be changed after initialization
                            if (plupload.inArray(option, [
                                'container',
                                'browse_button',
                                'drop_element',
                                'runtimes',
                                'multi_selection',
                                'flash_swf_url',
                                'silverlight_xap_url'
                            ]) > -1) {
                                return this.trigger('Error', {
                                    code: plupload.OPTION_ERROR,
                                    message: plupload.sprintf(plupload.translate("%s option cannot be changed.")),
                                    option: option
                                });
                            }
                        }

                        if (typeof(option) !== 'object') {
                            value = normalizeOption(option, value, this._options);

                            // queues will take in only appropriate options
                            if (_queueUpload) {
                                _queueUpload.setOption(option, value);
                            }
                            if (_queueResize) {
                                _queueResize.setOption(option, value);
                            }
                        }

                        Uploader.prototype.setOption.call(this, option, value);
                    },

                    /**
                     * Refreshes the upload instance by dispatching out a refresh event to all runtimes.
                     * This would for example reposition flash/silverlight shims on the page.
                     *
                     * @method refresh
                     */
                    refresh: function () {
                        if (_fileInputs.length) {
                            plupload.each(_fileInputs, function (fileInput) {
                                fileInput.trigger('Refresh');
                            });
                        }

                        if (_fileDrops.length) {
                            plupload.each(_fileDrops, function (fileDrops) {
                                fileDrops.trigger('Refresh');
                            });
                        }

                        this.trigger('Refresh');
                    },

                    /**
                     * Stops the upload of the queued files.
                     *
                     * @method stop
                     */
                    stop: function () {
                        if (Uploader.prototype.stop.call(this) && this.state != plupload.STOPPED) {
                            this.trigger('CancelUpload');
                        }
                    },


                    /**
                     * Disables/enables browse button on request.
                     *
                     * @method disableBrowse
                     * @param {Boolean} disable Whether to disable or enable (default: true)
                     */
                    disableBrowse: function () {
                        _disabled = arguments[0] !== undef ? arguments[0] : true;

                        if (_fileInputs.length) {
                            plupload.each(_fileInputs, function (fileInput) {
                                fileInput.disable(_disabled);
                            });
                        }

                        this.trigger('DisableBrowse', _disabled);
                    },

                    /**
                     * Returns the specified FileUploader object by id
                     *
                     * @method getFile
                     * @deprecated use getItem()
                     * @param {String} id FileUploader id to look for
                     * @return {plupload.FileUploader}
                     */
                    getFile: function (id) {
                        return this.getItem(id);
                    },

                    /**
                     * Adds file to the queue programmatically. Can be native file, instance of Plupload.File,
                     * instance of mOxie.File, input[type="file"] element, or array of these. Fires FilesAdded,
                     * if any files were added to the queue. Otherwise nothing happens.
                     *
                     * @method addFile
                     * @since 2.0
                     * @param {plupload.File|mOxie.File|File|Node|Array} file File or files to add to the queue.
                     * @param {String} [fileName] If specified, will be used as a name for the file
                     */
                    addFile: function (file, fileName) {
                        var self = this;
                        var queue = [];
                        var ruid; // spare runtime uid, for those files that do not have their own
                        var filesAdded = []; // here we track the files that got filtered and are added to the queue


                        function bindListeners(fileUp) {
                            fileUp.bind('beforestart', function (e) {
                                return self.trigger('BeforeUpload', e.target);
                            });

                            fileUp.bind('startupload', function () {
                                self.trigger('UploadFile', this);
                            });

                            fileUp.bind('progress', function () {
                                self.trigger('UploadProgress', this);
                            });

                            fileUp.bind('done', function (e, args) {
                                self.trigger('FileUploaded', this, args);
                            });

                            fileUp.bind('failed', function (e, err) {
                                self.trigger('Error', plupload.extend({
                                    code: plupload.HTTP_ERROR,
                                    message: plupload.translate('HTTP Error.'),
                                    file: this
                                }, err));
                            });
                        }


                        function filterFile(file, cb) {
                            var queue = [];
                            plupload.each(self.getOption('filters'), function (rule, name) {
                                if (fileFilters[name]) {
                                    queue.push(function (cb) {
                                        fileFilters[name].call(self, rule, file, function (res) {
                                            cb(!res);
                                        });
                                    });
                                }
                            });
                            plupload.inParallel(queue, cb);
                        }

                        /**
                         * @method resolveFile
                         * @private
                         * @param {mxiFile|mxiBlob|FileUploader|File|Blob|input[type="file"]} file
                         */
                        function resolveFile(file) {
                            var type = plupload.typeOf(file);

                            // mxiFile (final step for other conditional branches)
                            if (file instanceof moxie.file.File) {
                                if (!file.ruid && !file.isDetached()) {
                                    if (!ruid) { // weird case
                                        return false;
                                    }
                                    file.ruid = ruid;
                                    file.connectRuntime(ruid);
                                }

                                queue.push(function (cb) {
                                    // run through the internal and user-defined filters, if any
                                    filterFile(file, function (err) {
                                        var fileUp;

                                        if (!err) {
                                            fileUp = new PluploadFile(file, _queueUpload, _queueResize);

                                            if (fileName) {
                                                fileUp.name = fileName;
                                            }

                                            bindListeners(fileUp);

                                            self.addItem(fileUp); // make files available for the filters by updating the main queue directly
                                            filesAdded.push(fileUp);
                                            self.trigger("FileFiltered", fileUp);
                                        }

                                        plupload.delay(cb); // do not build up recursions or eventually we might hit the limits
                                    });
                                });
                            }
                            // mxiBlob
                            else if (file instanceof moxie.file.Blob) {
                                resolveFile(file.getSource());
                                file.destroy();
                            }
                            // native File or blob
                            else if (plupload.inArray(type, ['file', 'blob']) !== -1) {
                                resolveFile(new moxie.file.File(null, file));
                            }
                            // input[type="file"]
                            else if (type === 'node' && plupload.typeOf(file.files) === 'filelist') {
                                // if we are dealing with input[type="file"]
                                plupload.each(file.files, resolveFile);
                            }
                            // mixed array of any supported types (see above)
                            else if (type === 'array') {
                                fileName = null; // should never happen, but unset anyway to avoid funny situations
                                plupload.each(file, resolveFile);
                            }
                        }

                        ruid = getRUID();

                        resolveFile(file);

                        if (queue.length) {
                            plupload.inParallel(queue, function () {
                                // if any files left after filtration, trigger FilesAdded
                                if (filesAdded.length) {
                                    self.trigger("FilesAdded", filesAdded);
                                }
                            });
                        }
                    },

                    /**
                     * Removes a specific item from the queue
                     *
                     * @method removeFile
                     * @param {plupload.FileUploader|String} file
                     */
                    removeFile: function (file) {
                        var item = this.extractItem(typeof(file) === 'string' ? file : file.uid);
                        if (item) {
                            this.trigger("FilesRemoved", [item]);
                            item.destroy();
                        }
                    },

                    /**
                     * Removes part of the queue and returns removed files.
                     * Triggers FilesRemoved and consequently QueueChanged events.
                     *
                     * @method splice
                     * @param {Number} [start=0] Start index to remove from
                     * @param {Number} [length] Length of items to remove
                     */
                    splice: function () {
                        var i = 0;
                        var shouldRestart = plupload.STARTED == this.state;

                        var removed = Queue.prototype.splice.apply(this, arguments);
                        if (removed.length) {
                            this.trigger("FilesRemoved", removed);

                            if (shouldRestart) {
                                this.stop();
                            }

                            for (i = 0; i < removed.length; i++) {
                                removed[i].destroy();
                            }

                            if (shouldRestart) {
                                this.start();
                            }
                        }
                    },

                    /**
                     Dispatches the specified event name and its arguments to all listeners.

                     @method trigger
                     @param {String} name Event name to fire.
                     @param {Object..} Multiple arguments to pass along to the listener functions.
                     */

                    // override the parent method to match Plupload-like event logic
                    dispatchEvent: function (type) {
                        var list, args, result;

                        type = type.toLowerCase();

                        list = this.hasEventListener(type);

                        if (list) {
                            // sort event list by priority
                            list.sort(function (a, b) {
                                return b.priority - a.priority;
                            });

                            // first argument should be current plupload.Uploader instance
                            args = [].slice.call(arguments);
                            args.shift();
                            args.unshift(this);

                            for (var i = 0; i < list.length; i++) {
                                // Fire event, break chain if false is returned
                                if (list[i].fn.apply(list[i].scope, args) === false) {
                                    return false;
                                }
                            }
                        }
                        return true;
                    },

                    /**
                     Check whether uploader has any listeners to the specified event.

                     @method hasEventListener
                     @param {String} name Event name to check for.
                     */


                    /**
                     Adds an event listener by name.

                     @method bind
                     @param {String} name Event name to listen for.
                     @param {function} fn Function to call ones the event gets fired.
                     @param {Object} [scope] Optional scope to execute the specified function in.
                     @param {Number} [priority=0] Priority of the event handler - handlers with higher priorities will be called first
                     */
                    bind: function (name, fn, scope, priority) {
                        // adapt moxie EventTarget style to Plupload-like
                        plupload.Uploader.prototype.bind.call(this, name, fn, priority, scope);
                    }

                    /**
                     Removes the specified event listener.

                     @method unbind
                     @param {String} name Name of event to remove.
                     @param {function} fn Function to remove from listener.
                     */

                    /**
                     Removes all event listeners.

                     @method unbindAll
                     */
                });


                // keep alive deprecated properties
                if (_options.backward_compatibility) {
                    this.bind('FilesAdded FilesRemoved', function (up) {
                        up.files = up.toArray();
                    }, this, 999);

                    this.bind('OptionChanged', function (up, name, value) {
                        up.settings[name] = typeof(value) == 'object' ? plupload.extend({}, value) : value;
                    }, this, 999);
                }


                function getRUID() {
                    var ctrl = _fileInputs[0] || _fileDrops[0];
                    if (ctrl) {
                        return ctrl.getRuntime().uid;
                    }
                    return false;
                }


                function bindEventListeners() {
                    this.bind('FilesAdded FilesRemoved', function (up) {
                        up.trigger('QueueChanged');
                        up.refresh();
                    }, this, 999);

                    this.bind('BeforeUpload', onBeforeUpload);

                    this.bind('Stopped', function (up) {
                        up.trigger('UploadComplete');
                    });

                    this.bind('Error', onError);

                    this.bind('Destroy', onDestroy);
                }


                function initControls(cb) {
                    var self = this;
                    var initialized = 0;
                    var queue = [];

                    // common settings
                    var options = {
                        runtime_order: self.getOption('runtimes'),
                        required_caps: self.getOption('required_features'),
                        preferred_caps: self.getOption('preferred_caps'),
                        swf_url: self.getOption('flash_swf_url'),
                        xap_url: self.getOption('silverlight_xap_url')
                    };

                    // add runtime specific options if any
                    plupload.each(self.getOption('runtimes').split(/\s*,\s*/), function (runtime) {
                        if (self.getOption(runtime)) {
                            options[runtime] = self.getOption(runtime);
                        }
                    });

                    // initialize file pickers - there can be many
                    if (self.getOption('browse_button')) {
                        plupload.each(self.getOption('browse_button'), function (el) {
                            queue.push(function (cb) {
                                var fileInput = new moxie.file.FileInput(plupload.extend({}, options, {
                                    accept: self.getOption('filters').mime_types,
                                    name: self.getOption('file_data_name'),
                                    multiple: self.getOption('multi_selection'),
                                    container: self.getOption('container'),
                                    browse_button: el
                                }));

                                fileInput.onready = function () {
                                    var info = plupload.Runtime.getInfo(this.ruid);

                                    // for backward compatibility
                                    plupload.extend(self.features, {
                                        chunks: info.can('slice_blob'),
                                        multipart: info.can('send_multipart'),
                                        multi_selection: info.can('select_multiple')
                                    });

                                    initialized++;
                                    _fileInputs.push(this);
                                    cb();
                                };

                                fileInput.onchange = function () {
                                    self.addFile(this.files);
                                };

                                fileInput.bind('mouseenter mouseleave mousedown mouseup', function (e) {
                                    if (!_disabled) {
                                        if (self.getOption('browse_button_hover')) {
                                            if ('mouseenter' === e.type) {
                                                plupload.addClass(el, self.getOption('browse_button_hover'));
                                            } else if ('mouseleave' === e.type) {
                                                plupload.removeClass(el, self.getOption('browse_button_hover'));
                                            }
                                        }

                                        if (self.getOption('browse_button_active')) {
                                            if ('mousedown' === e.type) {
                                                plupload.addClass(el, self.getOption('browse_button_active'));
                                            } else if ('mouseup' === e.type) {
                                                plupload.removeClass(el, self.getOption('browse_button_active'));
                                            }
                                        }
                                    }
                                });

                                fileInput.bind('mousedown', function () {
                                    self.trigger('Browse');
                                });

                                fileInput.bind('error runtimeerror', function () {
                                    fileInput = null;
                                    cb();
                                });

                                fileInput.init();
                            });
                        });
                    }

                    // initialize drop zones
                    if (self.getOption('drop_element')) {
                        plupload.each(self.getOption('drop_element'), function (el) {
                            queue.push(function (cb) {
                                var fileDrop = new moxie.file.FileDrop(plupload.extend({}, options, {
                                    drop_zone: el
                                }));

                                fileDrop.onready = function () {
                                    var info = plupload.Runtime.getInfo(this.ruid);

                                    // for backward compatibility
                                    plupload.extend(self.features, {
                                        chunks: info.can('slice_blob'),
                                        multipart: info.can('send_multipart'),
                                        dragdrop: info.can('drag_and_drop')
                                    });

                                    initialized++;
                                    _fileDrops.push(this);
                                    cb();
                                };

                                fileDrop.ondrop = function () {
                                    self.addFile(this.files);
                                };

                                fileDrop.bind('error runtimeerror', function () {
                                    fileDrop = null;
                                    cb();
                                });

                                fileDrop.init();
                            });
                        });
                    }


                    plupload.inParallel(queue, function () {
                        if (typeof(cb) === 'function') {
                            cb(initialized);
                        }
                    });
                }


                // Internal event handlers
                function onBeforeUpload(up, file) {
                    // Generate unique target filenames
                    if (up.getOption('unique_names')) {
                        var matches = file.name.match(/\.([^.]+)$/),
                            ext = "part";
                        if (matches) {
                            ext = matches[1];
                        }
                        file.target_name = file.id + '.' + ext;
                    }
                }


                function onError(up, err) {
                    if (err.code === plupload.INIT_ERROR) {
                        up.destroy();
                    }
                    else if (err.code === plupload.HTTP_ERROR && up.state == plupload.STARTED) {
                        up.trigger('CancelUpload');
                    }
                }


                function onDestroy(up) {
                    up.forEachItem(function (file) {
                        file.destroy();
                    });

                    if (_fileInputs.length) {
                        plupload.each(_fileInputs, function (fileInput) {
                            fileInput.destroy();
                        });
                        _fileInputs = [];
                    }

                    if (_fileDrops.length) {
                        plupload.each(_fileDrops, function (fileDrop) {
                            fileDrop.destroy();
                        });
                        _fileDrops = [];
                    }

                    _initialized = false;

                    if (_queueUpload) {
                        _queueUpload.destroy();
                    }

                    if (_queueResize) {
                        _queueResize.destroy();
                    }

                    _options = _queueUpload = _queueResize = null; // purge these exclusively

                }

            }


            // convert plupload features to caps acceptable by mOxie
            function normalizeCaps(settings) {
                var features = settings.required_features,
                    caps = {};

                function resolve(feature, value, strict) {
                    // Feature notation is deprecated, use caps (this thing here is required for backward compatibility)
                    var map = {
                        chunks: 'slice_blob',
                        jpgresize: 'send_binary_string',
                        pngresize: 'send_binary_string',
                        progress: 'report_upload_progress',
                        multi_selection: 'select_multiple',
                        dragdrop: 'drag_and_drop',
                        drop_element: 'drag_and_drop',
                        headers: 'send_custom_headers',
                        urlstream_upload: 'send_binary_string',
                        canSendBinary: 'send_binary',
                        triggerDialog: 'summon_file_dialog'
                    };

                    if (map[feature]) {
                        caps[map[feature]] = value;
                    } else if (!strict) {
                        caps[feature] = value;
                    }
                }

                if (typeof(features) === 'string') {
                    plupload.each(features.split(/\s*,\s*/), function (feature) {
                        resolve(feature, true);
                    });
                } else if (typeof(features) === 'object') {
                    plupload.each(features, function (value, feature) {
                        resolve(feature, value);
                    });
                } else if (features === true) {
                    // check settings for required features
                    if (settings.chunk_size && settings.chunk_size > 0) {
                        caps.slice_blob = true;
                    }

                    if (!plupload.isEmptyObj(settings.resize) || settings.multipart === false) {
                        caps.send_binary_string = true;
                    }

                    if (settings.http_method) {
                        caps.use_http_method = settings.http_method;
                    }

                    plupload.each(settings, function (value, feature) {
                        resolve(feature, !!value, true); // strict check
                    });
                }

                return caps;
            }

            function normalizeOptions(options) {
                plupload.each(options, function (value, option) {
                    options[option] = normalizeOption(option, value, options);
                });
                return options;
            }

            /**
             Normalize an option.

             @method normalizeOption
             @private

             @param {String} option Name of the option to normalize
             @param {Mixed} value
             @param {Object} options The whole set of options, that might be modified during normalization (see max_file_size or unique_names)!
             */
            function normalizeOption(option, value, options) {
                switch (option) {

                    case 'chunk_size':
                        if (value = plupload.parseSize(value)) {
                            options.send_file_name = true;
                        }
                        break;

                    case 'headers':
                        var headers = {};
                        if (typeof(value) === 'object') {
                            plupload.each(value, function (value, key) {
                                headers[key.toLowerCase()] = value;
                            });
                        }
                        return headers;

                    case 'http_method':
                        return value.toUpperCase() === 'PUT' ? 'PUT' : 'POST';


                    case 'filters':
                        if (plupload.typeOf(value) === 'array') { // for backward compatibility
                            value = {
                                mime_types: value
                            };
                        }

                        // if file format filters are being updated, regenerate the matching expressions
                        if (value.mime_types) {
                            if (plupload.typeOf(value.mime_types) === 'string') {
                                value.mime_types = plupload.mimes2extList(value.mime_types);
                            }

                            // generate and cache regular expression for filtering file extensions
                            options.re_ext_filter = (function (filters) {
                                var extensionsRegExp = [];

                                plupload.each(filters, function (filter) {
                                    plupload.each(filter.extensions.split(/,/), function (ext) {
                                        if (/^\s*\*\s*$/.test(ext)) {
                                            extensionsRegExp.push('\\.*');
                                        } else {
                                            extensionsRegExp.push('\\.' + ext.replace(new RegExp('[' + ('/^$.*+?|()[]{}\\'.replace(/./g, '\\$&')) + ']', 'g'), '\\$&'));
                                        }
                                    });
                                });

                                return new RegExp('(' + extensionsRegExp.join('|') + ')$', 'i');
                            }(value.mime_types));
                        }

                        return value;

                    case 'max_file_size':
                        if (options && !options.filters) {
                            options.filters = {};
                        }
                        options.filters.max_file_size = value;
                        break;

                    case 'multipart':
                        if (!value) {
                            options.send_file_name = true;
                        }
                        break;

                    case 'multipart_params':
                        options.params = options.multipart_params = value;
                        break;

                    case 'resize':
                        if (value) {
                            return plupload.extend({
                                preserve_headers: true,
                                crop: false
                            }, value);
                        }
                        return false;

                    case 'prevent_duplicates':
                        if (options && !options.filters) {
                            options.filters = {};
                        }
                        options.filters.prevent_duplicates = !!value;
                        break;

                    case 'unique_names':
                        if (value) {
                            options.send_file_name = true;
                        }
                        break;

                    case 'required_features':
                        // Normalize the list of required capabilities
                        return normalizeCaps(plupload.extend({}, options));

                    case 'preferred_caps':
                        // Come up with the list of capabilities that can affect default mode in a multi-mode runtimes
                        return normalizeCaps(plupload.extend({}, options, {
                            required_features: true
                        }));

                    // options that require reinitialisation
                    case 'container':
                    case 'browse_button':
                    case 'drop_element':
                        return 'container' === option ? plupload.get(value) : plupload.getAll(value);
                }

                return value;
            }


            /**
             * Registers a filter that will be executed for each file added to the queue.
             * If callback returns false, file will not be added.
             *
             * Callback receives two arguments: a value for the filter as it was specified in settings.filters
             * and a file to be filtered. Callback is executed in the context of uploader instance.
             *
             * @method addFileFilter
             * @static
             * @param {String} name Name of the filter by which it can be referenced in settings.filters
             * @param {String} cb Callback - the actual routine that every added file must pass
             */
            function addFileFilter(name, cb) {
                fileFilters[name] = cb;
            }


            /**
             * A way to predict what runtime will be choosen in the current environment with the
             * specified settings.
             *
             * @method predictRuntime
             * @static
             * @param {Object|String} config Plupload settings to check
             * @param {String} [runtimes] Comma-separated list of runtimes to check against
             * @return {String} Type of compatible runtime
             */
            function predictRuntime(config, runtimes) {
                var up, runtime;

                up = new Uploader(config);
                runtime = plupload.Runtime.thatCan(up.getOption('required_features'), runtimes || config.runtimes);
                up.destroy();
                return runtime;
            }


            addFileFilter('mime_types', function (filters, file, cb) {
                if (filters.length && !this.getOption('re_ext_filter').test(file.name)) {
                    this.trigger('Error', {
                        code: plupload.FILE_EXTENSION_ERROR,
                        message: plupload.translate('File extension error.'),
                        file: file
                    });
                    cb(false);
                } else {
                    cb(true);
                }
            });


            addFileFilter('max_file_size', function (maxSize, file, cb) {
                var undef;

                maxSize = plupload.parseSize(maxSize);

                // Invalid file size
                if (file.size !== undef && maxSize && file.size > maxSize) {
                    this.trigger('Error', {
                        code: plupload.FILE_SIZE_ERROR,
                        message: plupload.translate('File size error.'),
                        file: file
                    });
                    cb(false);
                } else {
                    cb(true);
                }
            });


            addFileFilter('prevent_duplicates', function (value, file, cb) {
                var self = this;
                if (value) {
                    this.forEachItem(function (item) {
                        // Compare by name and size (size might be 0 or undefined, but still equivalent for both)
                        if (file.name === item.name && file.size === item.size) {
                            self.trigger('Error', {
                                code: plupload.FILE_DUPLICATE_ERROR,
                                message: plupload.translate('Duplicate file error.'),
                                file: file
                            });
                            cb(false);
                            return;
                        }
                    });
                }
                cb(true);
            });


            addFileFilter('prevent_empty', function (value, file, cb) {
                if (value && !file.size && file.size !== undef) {
                    this.trigger('Error', {
                        code: plupload.FILE_SIZE_ERROR,
                        message: plupload.translate('File size error.'),
                        file: file
                    });
                    cb(false);
                } else {
                    cb(true);
                }
            });


            Uploader.addFileFilter = addFileFilter;

            plupload.inherit(Uploader, Queue);

            // for backward compatibility
            plupload.addFileFilter = addFileFilter;
            plupload.predictRuntime = predictRuntime;

            return Uploader;
        });

        expose(["plupload", "plupload/core/Collection", "plupload/core/ArrCollection", "plupload/core/Optionable", "plupload/core/Queueable", "plupload/core/Stats", "plupload/core/Queue", "plupload/QueueUpload", "plupload/QueueResize", "plupload/ChunkUploader", "plupload/FileUploader", "plupload/ImageResizer", "plupload/File", "plupload/Uploader"]);
    })(this);
}));